View Javadoc

1   /**
2    * Copyright 2004-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.hr.lm.accrual.service;
17  
18  import java.math.BigDecimal;
19  import java.sql.Date;
20  import java.sql.Timestamp;
21  import java.util.ArrayList;
22  import java.util.Calendar;
23  import java.util.GregorianCalendar;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  
30  import org.apache.commons.collections.CollectionUtils;
31  import org.apache.commons.lang.StringUtils;
32  import org.apache.log4j.Logger;
33  import org.joda.time.DateTime;
34  import org.joda.time.Interval;
35  import org.kuali.hr.job.Job;
36  import org.kuali.hr.lm.LMConstants;
37  import org.kuali.hr.lm.accrual.AccrualCategory;
38  import org.kuali.hr.lm.accrual.AccrualCategoryRule;
39  import org.kuali.hr.lm.accrual.PrincipalAccrualRan;
40  import org.kuali.hr.lm.accrual.RateRange;
41  import org.kuali.hr.lm.accrual.RateRangeAggregate;
42  import org.kuali.hr.lm.leaveblock.LeaveBlock;
43  import org.kuali.hr.lm.leaveplan.LeavePlan;
44  import org.kuali.hr.lm.timeoff.SystemScheduledTimeOff;
45  import org.kuali.hr.lm.workflow.LeaveCalendarDocumentHeader;
46  import org.kuali.hr.time.assignment.Assignment;
47  import org.kuali.hr.time.calendar.CalendarEntries;
48  import org.kuali.hr.time.earncode.EarnCode;
49  import org.kuali.hr.time.principal.PrincipalHRAttributes;
50  import org.kuali.hr.time.service.base.TkServiceLocator;
51  import org.kuali.hr.time.util.TKContext;
52  import org.kuali.hr.time.util.TKUtils;
53  import org.kuali.hr.time.util.TkConstants;
54  
55  import edu.emory.mathcs.backport.java.util.Collections;
56  
57  public class AccrualServiceImpl implements AccrualService {
58      private static final Logger LOG = Logger.getLogger(AccrualServiceImpl.class);
59  
60  	@Override
61  	public void runAccrual(String principalId) {
62  		Date startDate = getStartAccrualDate(principalId);
63  		Date endDate = getEndAccrualDate(principalId);
64  
65  		LOG.info("AccrualServiceImpl.runAccrual() STARTED with Principal: "+principalId);
66  		runAccrual(principalId,startDate,endDate, true);
67  		
68  	}
69  	
70  	@Override
71  	public void runAccrual(String principalId, Date startDate, Date endDate, boolean recordRanData) {
72  		runAccrual(principalId, startDate, endDate, recordRanData, TKContext.getPrincipalId());
73  	}
74  
75  	@SuppressWarnings("unchecked")
76  	@Override
77  	public void runAccrual(String principalId, Date startDate, Date endDate, boolean recordRanData, String runAsPrincipalId) {
78  		List<LeaveBlock> accrualLeaveBlocks = new ArrayList<LeaveBlock>();
79  		Map<String, BigDecimal> accumulatedAccrualCatToAccrualAmounts = new HashMap<String,BigDecimal>();
80  		Map<String, BigDecimal> accumulatedAccrualCatToNegativeAccrualAmounts = new HashMap<String,BigDecimal>();
81  		
82  		if (startDate != null && endDate != null) {
83              LOG.info("AccrualServiceImpl.runAccrual() STARTED with Principal: "+principalId+" Start: "+startDate.toString()+" End: "+endDate.toString());
84  		}
85  		if(startDate.after(endDate)) {
86  			throw new RuntimeException("Start Date " + startDate.toString() + " should not be later than End Date " + endDate.toString());
87  		}
88  		//Inactivate all previous accrual-generated entries for this span of time
89  		deactivateOldAccruals(principalId, startDate, endDate, runAsPrincipalId);
90  		
91  		//Build a rate range aggregate with appropriate information for this period of time detailing Rate Ranges for job
92  		//entries for this range of time
93  		RateRangeAggregate rrAggregate = this.buildRateRangeAggregate(principalId, startDate, endDate);	
94  		PrincipalHRAttributes phra = null;
95  		PrincipalHRAttributes endPhra = null;
96  		LeavePlan lp = null;
97  		List<AccrualCategory> accrCatList = null;
98  		
99  		//Iterate over every day in span 
100 		Calendar aCal = Calendar.getInstance();
101 		aCal.setTime(startDate);
102 		while (!aCal.getTime().after(endDate)) {
103 			java.util.Date currentDate = aCal.getTime();
104 			RateRange currentRange = rrAggregate.getRateOnDate(currentDate);
105 			if(currentRange == null) {
106 				aCal.add(Calendar.DATE, 1);
107 				continue;
108 			}
109 			
110 			phra = currentRange.getPrincipalHRAttributes();
111 			if(phra == null || TKUtils.removeTime(currentDate).before(TKUtils.removeTime(phra.getServiceDate()))) {
112 				aCal.add(Calendar.DATE, 1);
113 				continue;
114 			}
115 			
116 			
117 	// use the effectiveDate of this principalHRAttribute to search for inactive entries for this principalId
118 	// If there's an inactive entry, it means the job is going to end on the effectiveDate of the inactive entry
119 	// used for minimumPercentage and proration
120 			endPhra = currentRange.getEndPrincipalHRAttributes();
121 			if(endPhra != null && TKUtils.removeTime(currentDate).after(TKUtils.removeTime(endPhra.getEffectiveDate()))) {
122 				aCal.add(Calendar.DATE, 1);
123 				continue;
124 			}
125 			
126 	// if the date range is before the service date of this employee, do not calculate accrual
127 			if(endDate.before(phra.getServiceDate())) {
128 				return;
129 			}
130 			lp = currentRange.getLeavePlan();
131 			accrCatList = currentRange.getAcList();
132 			// if the employee status is changed, create an empty leave block on the currentDate
133 			if(currentRange.isStatusChanged()) {
134 				this.createEmptyLeaveBlockForStatusChange(principalId, accrualLeaveBlocks, currentDate);
135 			}
136 			// if no job found for the employee on the currentDate, do nothing
137 			if(CollectionUtils.isEmpty(currentRange.getJobs())) {
138 				aCal.add(Calendar.DATE, 1);
139 				continue;
140 			}
141 			
142 			BigDecimal ftePercentage = currentRange.getAccrualRatePercentageModifier();
143 			BigDecimal totalOfStandardHours = currentRange.getStandardHours();
144 			boolean fullFteGranted = false;
145 			for(AccrualCategory anAC : accrCatList) {
146 				fullFteGranted = false;
147 				if(!currentDate.before(phra.getEffectiveDate()) && !anAC.getAccrualEarnInterval().equals("N")) {   	// "N" means no accrual
148 					boolean prorationFlag = this.getProrationFlag(anAC.getProration());
149 					// get the accrual rule 
150 					AccrualCategoryRule currentAcRule = this.getRuleForAccrualCategory(currentRange.getAcRuleList(), anAC);
151 				
152 					// check if accrual category rule changed
153 					if(currentAcRule != null) {
154 						java.util.Date ruleStartDate = getRuleStartDate(currentAcRule.getServiceUnitOfTime(), phra.getServiceDate(), currentAcRule.getStart());
155 						Date ruleStartSqlDate = new java.sql.Date(ruleStartDate.getTime());
156 						java.util.Date previousIntervalDay = this.getPrevIntervalDate(ruleStartSqlDate, anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap());
157 						java.util.Date nextIntervalDay = this.getNextIntervalDate(ruleStartSqlDate, anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap());
158 						
159 						RateRange previousRange = rrAggregate.getRateOnDate(previousIntervalDay);
160 						AccrualCategoryRule previousAcRule = null;
161 						if(previousRange != null) {
162 							previousAcRule = this.getRuleForAccrualCategory(previousRange.getAcRuleList(), anAC);
163 						}
164 						// rule changed
165 						if(previousAcRule != null && !previousAcRule.getLmAccrualCategoryRuleId().equals(currentAcRule.getLmAccrualCategoryRuleId())) {
166 							if(TKUtils.removeTime(currentDate).compareTo(TKUtils.removeTime(previousIntervalDay)) >= 0 
167 									&& TKUtils.removeTime(currentDate).compareTo(TKUtils.removeTime(nextIntervalDay)) <= 0) {
168 								int workDaysInBetween = TKUtils.getWorkDays(ruleStartSqlDate, nextIntervalDay);
169 								boolean minReachedFlag = minimumPercentageReachedForPayPeriod(anAC.getMinPercentWorked(), 
170 												anAC.getAccrualEarnInterval(), workDaysInBetween, new java.sql.Date(nextIntervalDay.getTime()),
171 												phra.getPayCalendar(), rrAggregate.getCalEntryMap());
172 								if(prorationFlag) {
173 									if(minReachedFlag) {
174 										// min reached, proration=true, rule changed, then use actual work days of currentRule for calculation
175 										// so nothing special needs to be done here								
176 									} else {
177 										//minimum percentage NOT reached, proration = true, rule changed, then use previousRule for the whole pay period
178 										currentAcRule = previousAcRule;
179 									}
180 								} else {
181 									if(minReachedFlag) {
182 										// min reached, proration = false, rule changed, then accrual the whole fte of the new rule for this pay interval
183 										accumulatedAccrualCatToAccrualAmounts.put(anAC.getLmAccrualCategoryId(), currentAcRule.getAccrualRate());
184 										fullFteGranted = true;
185 									} else {
186 										//min NOT reached, proration = false, rule changed, then accrual the whole fte of the previous rule for this pay interval
187 										accumulatedAccrualCatToAccrualAmounts.put(anAC.getLmAccrualCategoryId(), previousAcRule.getAccrualRate());
188 										fullFteGranted = true;
189 									}
190 								}
191 							}
192 						}
193 					}
194 					
195 					// check for first pay period of principal attributes considering minimum percentage and proration	
196 					java.util.Date firstIntervalDate = this.getNextIntervalDate(phra.getEffectiveDate(), anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap());
197 					if(!TKUtils.removeTime(currentDate).before(TKUtils.removeTime(phra.getEffectiveDate())) 
198 							&& !TKUtils.removeTime(currentDate).after(TKUtils.removeTime(firstIntervalDate))) {
199 						int workDaysInBetween = TKUtils.getWorkDays(phra.getEffectiveDate(), firstIntervalDate);
200 						boolean minReachedFlag = minimumPercentageReachedForPayPeriod(anAC.getMinPercentWorked(),  anAC.getAccrualEarnInterval(), 
201 									workDaysInBetween, new java.sql.Date(firstIntervalDate.getTime()),
202 									phra.getPayCalendar(), rrAggregate.getCalEntryMap());
203 						
204 						if(prorationFlag) {
205 							if(minReachedFlag) {
206 								// minimum reached, proration = true, first pay period, then use actual work days of currentRule for calculation
207 								// so nothing special needs to be done here
208 							} else {
209 								// min NOT reached, proration = true, first pay period, then no accrual for this pay period
210 								accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
211 								accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
212 								continue;
213 							}
214 						} else {
215 							if(minReachedFlag) {
216 								//  minimum reached, proration = false, first pay period, then accrual the whole fte of current AC rule for this pay interval
217 								accumulatedAccrualCatToAccrualAmounts.put(anAC.getLmAccrualCategoryId(), currentAcRule.getAccrualRate());
218 								fullFteGranted = true;
219 							} else {
220 								// min NOT reached, proration = false, first pay period, then no accrual for this pay period
221 								accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
222 								accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
223 								continue;
224 							}
225 						}
226 					}
227 					// last accrual interval
228 					if(endPhra != null) {	// the employment is going to end on the effectiveDate of enPhra
229 						java.util.Date previousIntervalDate = this.getPrevIntervalDate(endPhra.getEffectiveDate(), anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap());
230 						// currentDate is between the end date and the last interval date, so we are in the last interval
231 						if(!TKUtils.removeTime(currentDate).after(TKUtils.removeTime(endPhra.getEffectiveDate())) 
232 								&& TKUtils.removeTime(currentDate).after(TKUtils.removeTime(previousIntervalDate))) {
233 							java.util.Date lastIntervalDate = this.getNextIntervalDate(endPhra.getEffectiveDate(), anAC.getAccrualEarnInterval(),  phra.getPayCalendar(), rrAggregate.getCalEntryMap());
234 							int workDaysInBetween = TKUtils.getWorkDays(previousIntervalDate, endPhra.getEffectiveDate());
235 							boolean minReachedFlag = minimumPercentageReachedForPayPeriod(anAC.getMinPercentWorked(),  anAC.getAccrualEarnInterval(), 
236 										workDaysInBetween, new java.sql.Date(lastIntervalDate.getTime()),
237 										phra.getPayCalendar(), rrAggregate.getCalEntryMap());
238 							if(prorationFlag) {
239 								if(minReachedFlag) {
240 									// minimum reached, proration = true, first pay period, then use actual work days of currentRule for calculation
241 									// so nothing special needs to be done here
242 								} else {
243 									// min NOT reached, proration = true, first pay period, then no accrual for this pay period
244 									accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
245 									accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
246 									continue;
247 								}
248 							} else {
249 								if(minReachedFlag) {
250 									//  minimum reached, proration = false, first pay period, then accrual the whole fte of current AC rule for this pay interval
251 									accumulatedAccrualCatToAccrualAmounts.put(anAC.getLmAccrualCategoryId(), currentAcRule.getAccrualRate());
252 									fullFteGranted = true;
253 								} else {
254 									// min NOT reached, proration = false, first pay period, then no accrual for this pay period
255 									accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
256 									accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
257 									continue;
258 								}
259 							}
260 						}
261 					}
262 										
263 					if(currentAcRule == null) {
264 						accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
265 						accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId());
266 						continue;
267 					}
268 					
269 					// only accrual on work days
270 					if(!TKUtils.isWeekend(currentDate) && !fullFteGranted) {
271 						BigDecimal accrualRate = currentAcRule.getAccrualRate();
272 						int numberOfWorkDays = this.getWorkDaysInInterval(new java.sql.Date(currentDate.getTime()), anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap());
273 						BigDecimal dayRate = numberOfWorkDays > 0 ? accrualRate.divide(new BigDecimal(numberOfWorkDays), 6, BigDecimal.ROUND_HALF_UP) : new BigDecimal(0);
274 						//Fetch the accural rate based on rate range for today(Rate range is the accumulated list of jobs and accrual rate for today)
275 						//Add to total accumulatedAccrualCatToAccrualAmounts
276 						//use rule and ftePercentage to calculate the hours						
277 						this.calculateHours(anAC.getLmAccrualCategoryId(), ftePercentage, dayRate, accumulatedAccrualCatToAccrualAmounts);
278 						
279 						//get not eligible for accrual hours based on leave block on this day
280 						BigDecimal noAccrualHours = getNotEligibleForAccrualHours(principalId, new java.sql.Date(currentDate.getTime()));
281 						
282 						if(noAccrualHours.compareTo(BigDecimal.ZERO) != 0 && totalOfStandardHours.compareTo(BigDecimal.ZERO) != 0) {
283 							BigDecimal dayHours = totalOfStandardHours.divide(new BigDecimal(5), 6, BigDecimal.ROUND_HALF_UP);
284 							BigDecimal noAccrualRate = dayRate.multiply(noAccrualHours.divide(dayHours));
285 							this.calculateHours(anAC.getLmAccrualCategoryId(), ftePercentage, noAccrualRate, accumulatedAccrualCatToNegativeAccrualAmounts);
286 						}
287 					}					
288 					//Determine if we are at the accrual earn interval in the span, if so add leave block for accumulated accrual amount to list
289 					//and reset accumulatedAccrualCatToAccrualAmounts and accumulatedAccrualCatToNegativeAccrualAmounts for this accrual category
290 					if(this.isDateAnIntervalDate(currentDate, anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap())) {
291 						BigDecimal acHours = accumulatedAccrualCatToAccrualAmounts.get(anAC.getLmAccrualCategoryId());
292 						
293 						if(acHours != null) {
294 							createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, acHours, anAC, null, true, currentRange.getLeaveCalendarDocumentId());
295 							accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId());	// reset accumulatedAccrualCatToAccrualAmounts
296 							fullFteGranted = false;
297 						}
298 						
299 						BigDecimal adjustmentHours = accumulatedAccrualCatToNegativeAccrualAmounts.get(anAC.getLmAccrualCategoryId());
300 						if(adjustmentHours != null && adjustmentHours.compareTo(BigDecimal.ZERO) != 0) {
301 							// do not create leave block if the ajustment amount is 0
302 							createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, adjustmentHours, anAC, null, false, currentRange.getLeaveCalendarDocumentId());
303 							accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId());	// reset accumulatedAccrualCatToNegativeAccrualAmounts
304 						}
305 					}			
306 				}
307 			}
308 			//Determine if today is a system scheduled time off and accrue holiday if so.
309 			SystemScheduledTimeOff ssto = currentRange.getSysScheTimeOff();
310 			if(ssto != null) {
311 				AccrualCategory anAC = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(ssto.getAccrualCategory(), ssto.getEffectiveDate());
312 				if(anAC == null) {
313 					throw new RuntimeException("Cannot find Accrual Category for system scheduled time off " + ssto.getLmSystemScheduledTimeOffId());
314 				}
315 				BigDecimal hrs = ssto.getAmountofTime().multiply(ftePercentage);
316 				// system scheduled time off leave block
317 				createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, hrs, anAC, ssto.getLmSystemScheduledTimeOffId(), true, currentRange.getLeaveCalendarDocumentId());
318 				// usage leave block with negative amount
319 				createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, hrs.negate(), anAC, ssto.getLmSystemScheduledTimeOffId(), true, currentRange.getLeaveCalendarDocumentId());
320 			}
321 			// if today is the last day of the employment, create leave blocks if there's any hours available
322 			if(endPhra != null && TKUtils.removeTime(currentDate).equals(TKUtils.removeTime(endPhra.getEffectiveDate()))){
323 				// accumulated accrual amount
324 				if(!accumulatedAccrualCatToAccrualAmounts.isEmpty()) {
325 					for(Map.Entry<String, BigDecimal> entry : accumulatedAccrualCatToAccrualAmounts.entrySet()) {
326 						if(entry.getValue() != null && entry.getValue().compareTo(BigDecimal.ZERO) != 0) {
327 							AccrualCategory anAC = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(entry.getKey());
328 							createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, entry.getValue(), anAC, null, true, currentRange.getLeaveCalendarDocumentId());
329 						}
330 					}
331 					accumulatedAccrualCatToAccrualAmounts = new HashMap<String,BigDecimal>();	// reset accumulatedAccrualCatToAccrualAmounts
332 				}
333 				// negative/adjustment accrual amount
334 				if(!accumulatedAccrualCatToNegativeAccrualAmounts.isEmpty()) {
335 					for(Map.Entry<String, BigDecimal> entry : accumulatedAccrualCatToNegativeAccrualAmounts.entrySet()) {
336 						if(entry.getValue() != null && entry.getValue().compareTo(BigDecimal.ZERO) != 0) {
337 							AccrualCategory anAC = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(entry.getKey());
338 							createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, entry.getValue(), anAC, null, true, currentRange.getLeaveCalendarDocumentId());
339 						}
340 					}
341 					accumulatedAccrualCatToNegativeAccrualAmounts = new HashMap<String,BigDecimal>();	// reset accumulatedAccrualCatToNegativeAccrualAmounts
342 				}
343 				phra = null;	// reset principal attribute so new value will be retrieved
344 				endPhra = null;	// reset end principal attribute so new value will be retrieved
345 			}
346 			
347 			aCal.add(Calendar.DATE, 1);
348 		}
349 		
350 		//Save accrual leave blocks at the very end
351 		TkServiceLocator.getLeaveBlockService().saveLeaveBlocks(accrualLeaveBlocks);
352 		
353 		// record timestamp of this accrual run in database
354 		if(recordRanData) {
355 			TkServiceLocator.getPrincipalAccrualRanService().updatePrincipalAccrualRanInfo(principalId);
356 		}
357 		
358 	}
359 	
360 	private void deactivateOldAccruals(String principalId, Date startDate, Date endDate, String runAsPrincipalId) {
361 		List<LeaveBlock> previousLB = TkServiceLocator.getLeaveBlockService().getAccrualGeneratedLeaveBlocks(principalId, startDate, endDate);
362 		List<LeaveBlock> sstoAccrualList = new ArrayList<LeaveBlock>();
363 		List<LeaveBlock> sstoUsageList = new ArrayList<LeaveBlock>();
364 		
365 		for(LeaveBlock lb : previousLB) {
366 			if(StringUtils.isNotEmpty(lb.getScheduleTimeOffId())) {
367 				if(lb.getLeaveAmount().compareTo(BigDecimal.ZERO) > 0) {
368 					sstoAccrualList.add(lb);
369 				} else if(lb.getLeaveAmount().compareTo(BigDecimal.ZERO) < 0) {
370 					sstoUsageList.add(lb);
371 				}
372 			} else {
373 				if(!(StringUtils.equals(lb.getLeaveBlockType(),LMConstants.LEAVE_BLOCK_TYPE.BALANCE_TRANSFER) ||
374 						StringUtils.equals(lb.getLeaveBlockType(),LMConstants.LEAVE_BLOCK_TYPE.LEAVE_PAYOUT)))
375 					TkServiceLocator.getLeaveBlockService().deleteLeaveBlock(lb.getLmLeaveBlockId(), runAsPrincipalId);
376 			}
377 		}
378 		
379 		for(LeaveBlock accrualLb : sstoAccrualList) {
380 			for(LeaveBlock usageLb : sstoUsageList) {
381 				// both usage and accrual ssto leave blocks are there, so the ssto accural is not banked, removed both leave blocks
382 				// if this is no ssto usage leave block, it means the user has banked this ssto hours. Don't delete this ssto accrual leave block
383 				if(accrualLb.getScheduleTimeOffId().equals(usageLb.getScheduleTimeOffId())) {	
384 					TkServiceLocator.getLeaveBlockService().deleteLeaveBlock(accrualLb.getLmLeaveBlockId(), runAsPrincipalId);
385 					TkServiceLocator.getLeaveBlockService().deleteLeaveBlock(usageLb.getLmLeaveBlockId(), runAsPrincipalId);
386 				}
387 			}
388 		}
389 		
390 	}
391 	
392 	private BigDecimal getNotEligibleForAccrualHours(String principalId, Date currentDate) {
393 		BigDecimal hours = BigDecimal.ZERO;
394 		// check if there's any manual not-eligible-for-accrual leave blocks, use the hours of the leave block to adjust accrual calculation 
395 		List<LeaveBlock> lbs = TkServiceLocator.getLeaveBlockService().getNotAccrualGeneratedLeaveBlocksForDate(principalId, currentDate);
396 		for(LeaveBlock lb : lbs) {
397 			EarnCode ec = TkServiceLocator.getEarnCodeService().getEarnCode(lb.getEarnCode(), currentDate);
398 			if(ec == null) {
399 				throw new RuntimeException("Cannot find Earn Code for Leave block " + lb.getLmLeaveBlockId());
400 			}
401 			if(ec.getEligibleForAccrual().equals("N") && lb.getLeaveAmount().compareTo(BigDecimal.ZERO) != 0) {
402 				hours = hours.add(lb.getLeaveAmount());
403 			}		
404 		}
405 		return hours;
406 	}
407 	
408 	private void createLeaveBlock(String principalId, List<LeaveBlock> accrualLeaveBlocks, 
409 			java.util.Date currentDate, BigDecimal hrs, AccrualCategory anAC, String sysSchTimeOffId, 
410 			boolean createZeroLeaveBlock, String leaveDocId) {
411 		// Replacing Leave Code to earn code - KPME 1634
412 		EarnCode ec = TkServiceLocator.getEarnCodeService().getEarnCode(anAC.getEarnCode(), anAC.getEffectiveDate());
413 		if(ec == null) {
414 			throw new RuntimeException("Cannot find Earn Code for Accrual category " + anAC.getAccrualCategory());
415 		}
416 		// use rounding option and fract time allowed of Leave Code to round the leave block hours
417 		BigDecimal roundedHours = TkServiceLocator.getEarnCodeService().roundHrsWithEarnCode(hrs, ec);
418 		if(!createZeroLeaveBlock && roundedHours.compareTo(BigDecimal.ZERO) == 0) {
419 			return;	// do not create leave block with zero amount
420 		}
421 		LeaveBlock aLeaveBlock = new LeaveBlock();
422 		aLeaveBlock.setAccrualCategory(anAC.getAccrualCategory());
423 		aLeaveBlock.setLeaveDate(new java.sql.Date(currentDate.getTime()));
424 		aLeaveBlock.setPrincipalId(principalId);
425 		//More than one earn code can be associated with an accrual category. Which one does this get?
426 		aLeaveBlock.setEarnCode(anAC.getEarnCode());
427 		aLeaveBlock.setDateAndTime(new Timestamp(currentDate.getTime()));
428 		aLeaveBlock.setAccrualGenerated(true);
429 		aLeaveBlock.setBlockId(0L);
430 		aLeaveBlock.setScheduleTimeOffId(sysSchTimeOffId);
431 		aLeaveBlock.setLeaveAmount(roundedHours);
432 		aLeaveBlock.setLeaveBlockType(LMConstants.LEAVE_BLOCK_TYPE.ACCRUAL_SERVICE);
433 		aLeaveBlock.setRequestStatus(LMConstants.REQUEST_STATUS.APPROVED);
434 		aLeaveBlock.setDocumentId(leaveDocId);
435 		
436 		accrualLeaveBlocks.add(aLeaveBlock);
437 		
438 	}
439 	
440 	private void createEmptyLeaveBlockForStatusChange(String principalId, List<LeaveBlock> accrualLeaveBlocks, java.util.Date currentDate) {
441 		LeaveBlock aLeaveBlock = new LeaveBlock();
442 		aLeaveBlock.setAccrualCategory(null);
443 		aLeaveBlock.setLeaveDate(new java.sql.Date(currentDate.getTime()));
444 		aLeaveBlock.setPrincipalId(principalId);
445 		aLeaveBlock.setEarnCode(LMConstants.STATUS_CHANGE_EARN_CODE);	// fake leave code
446 		aLeaveBlock.setDateAndTime(new Timestamp(currentDate.getTime()));
447 		aLeaveBlock.setAccrualGenerated(true);
448 		aLeaveBlock.setBlockId(0L);
449 		aLeaveBlock.setScheduleTimeOffId(null);
450 		aLeaveBlock.setLeaveAmount(BigDecimal.ZERO);
451 		aLeaveBlock.setLeaveBlockType(LMConstants.LEAVE_BLOCK_TYPE.ACCRUAL_SERVICE);
452 		aLeaveBlock.setRequestStatus(LMConstants.REQUEST_STATUS.APPROVED);
453 		
454 		accrualLeaveBlocks.add(aLeaveBlock);
455 		
456 	}
457 
458 	private void calculateHours(String accrualCategoryId, BigDecimal fte, BigDecimal rate, Map<String, BigDecimal> accumulatedAmounts ) {
459 		BigDecimal hours = rate.multiply(fte);
460 		BigDecimal oldHours = accumulatedAmounts.get(accrualCategoryId);
461 		BigDecimal newHours = oldHours == null ? hours : hours.add(oldHours);
462 		accumulatedAmounts.put(accrualCategoryId, newHours);
463 	}
464 	
465 	public Date getStartAccrualDate(String principalId){
466 		return null;
467 	}
468 	
469 	public Date getEndAccrualDate(String principalId){
470 		//KPME-1246  Fetch planning months
471 		
472 		return null;
473 	}
474 
475 	@Override
476 	public void runAccrual(List<String> principalIds) {
477 		for(String principalId : principalIds){
478 			runAccrual(principalId);
479 		}
480 	}
481 	
482 	private boolean isDateAnIntervalDate(java.util.Date aDate, String earnInterval, String payCalName,  Map<String, List<CalendarEntries>> aMap) {
483 		if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) {
484 			return isDateAtPayCalInterval(aDate, earnInterval, payCalName, aMap);
485 		} else {
486 			return this.isDateAtEarnInterval(aDate, earnInterval);
487 		}
488 	}
489 	
490 	private boolean isDateAtPayCalInterval(java.util.Date aDate, String earnInterval, String payCalName,  Map<String, List<CalendarEntries>> aMap) {
491 		if(StringUtils.isNotEmpty(payCalName) 
492 				&& !aMap.isEmpty()
493 				&& earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) {	// only used for ac earn interval == pay calendar
494 			List<CalendarEntries> entryList = aMap.get(payCalName);
495 			if(CollectionUtils.isNotEmpty(entryList)) {
496 				for(CalendarEntries anEntry : entryList) {
497 					// endPeriodDate of calendar entry is the beginning hour of the next day, so we need to substract one day from it to get the real end date
498 					java.util.Date endDate = TKUtils.addDates(anEntry.getEndPeriodDate(), -1);
499 					if(aDate.compareTo(endDate) == 0) {
500 						return true;
501 					}
502 				}
503 			}
504 		}
505 		return false;
506 	}
507 	
508 	@Override
509 	public boolean isDateAtEarnInterval(java.util.Date aDate, String earnInterval) {
510 		boolean atEarnInterval = false;
511 		if(LMConstants.ACCRUAL_EARN_INTERVAL_MAP.containsKey(earnInterval)) {
512 			Calendar aCal = Calendar.getInstance();
513 			aCal.setTime(aDate);
514 			
515 			if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.DAILY)) {
516 				atEarnInterval = true;
517 			} else if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.WEEKLY)) {
518 				// figure out if the day is a Saturday
519 				if(aCal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
520 					atEarnInterval = true;
521 				}
522 			} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.SEMI_MONTHLY)) {
523 				// either the 15th or the last day of the month
524 				if(aCal.get(Calendar.DAY_OF_MONTH) == 15 || aCal.get(Calendar.DAY_OF_MONTH) == aCal.getActualMaximum(Calendar.DAY_OF_MONTH)) {
525 					atEarnInterval = true;
526 				}
527 			} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.MONTHLY)) {
528 				// the last day of the month
529 				if(aCal.get(Calendar.DAY_OF_MONTH) == aCal.getActualMaximum(Calendar.DAY_OF_MONTH)) {
530 					atEarnInterval = true;
531 				}
532 			} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.YEARLY)) {
533 				// the last day of the year
534 				if(aCal.get(Calendar.DAY_OF_YEAR) == aCal.getActualMaximum(Calendar.DAY_OF_YEAR)) {
535 					atEarnInterval = true;
536 				}
537 			}else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.NO_ACCRUAL)) {
538 				// no calculation
539 			}
540 		}
541 		return atEarnInterval;
542 	}
543 
544 
545 	@Override
546 	public RateRangeAggregate buildRateRangeAggregate(String principalId, Date startDate, Date endDate) {
547 		RateRangeAggregate rrAggregate = new RateRangeAggregate();
548 		List<RateRange> rateRangeList = new ArrayList<RateRange>();	
549 		Calendar gc = new GregorianCalendar();
550 		gc.setTime(startDate);
551 		// get all active jobs that are effective before the endDate
552 		List<Job> activeJobs = TkServiceLocator.getJobService().getAllActiveLeaveJobs(principalId, endDate);
553 		List<Job> inactiveJobs = TkServiceLocator.getJobService().getAllInActiveLeaveJobsInRange(principalId, startDate, endDate);
554 		
555 		List<PrincipalHRAttributes> phaList = TkServiceLocator.getPrincipalHRAttributeService().getAllActivePrincipalHrAttributesForPrincipalId(principalId, endDate);
556 		List<PrincipalHRAttributes> inactivePhaList = TkServiceLocator.getPrincipalHRAttributeService().getAllInActivePrincipalHrAttributesForPrincipalId(principalId, endDate);
557 		
558 		if(activeJobs.isEmpty() || phaList.isEmpty()) {
559 			return rrAggregate;
560 		}
561 		
562 		Set<String> phaLpSet = new HashSet<String>();
563 		Set<String> calNameSet = new HashSet<String>();
564 		if(CollectionUtils.isNotEmpty(phaList)) {
565 			for(PrincipalHRAttributes pha : phaList) {
566 				phaLpSet.add(pha.getLeavePlan());
567 				calNameSet.add(pha.getPayCalendar());
568 			}
569 		}
570 		
571 		List<LeavePlan> activeLpList = new ArrayList<LeavePlan> ();
572 		List<LeavePlan> inactiveLpList = new ArrayList<LeavePlan> ();
573 		for(String lpString : phaLpSet) {
574 			List<LeavePlan> aList = TkServiceLocator.getLeavePlanService().getAllActiveLeavePlan(lpString, endDate);
575 			activeLpList.addAll(aList);
576 			
577 			aList = TkServiceLocator.getLeavePlanService().getAllInActiveLeavePlan(lpString, endDate);
578 			inactiveLpList.addAll(aList);
579 		}
580 		
581 		// get all pay calendar entries for this employee. used to determine interval dates
582 		Map<String, List<CalendarEntries>> calEntryMap = new HashMap<String, List<CalendarEntries>>();
583 		for(String calName : calNameSet) {
584 			org.kuali.hr.time.calendar.Calendar aCal = TkServiceLocator.getCalendarService().getCalendarByGroup(calName);
585 			if(aCal != null) {
586 				List<CalendarEntries> aList = TkServiceLocator.getCalendarEntriesService().getAllCalendarEntriesForCalendarId(aCal.getHrCalendarId());
587 				Collections.sort(aList);
588 				calEntryMap.put(calName, aList);
589 			}
590 		}
591 		rrAggregate.setCalEntryMap(calEntryMap);		
592 		
593 		Set<String> lpStringSet = new HashSet<String>();
594 		if(CollectionUtils.isNotEmpty(activeLpList)) {
595 			for(LeavePlan lp : activeLpList) {
596 				lpStringSet.add(lp.getLeavePlan());
597 			}
598 		}
599 		List<SystemScheduledTimeOff> sstoList = new ArrayList<SystemScheduledTimeOff>();
600 		for(String lpString : lpStringSet) {
601 			List<SystemScheduledTimeOff> aList =TkServiceLocator.getSysSchTimeOffService().getSystemScheduledTimeOffsForLeavePlan(startDate, endDate, lpString);
602 			if(CollectionUtils.isNotEmpty(aList)) {
603 				sstoList.addAll(aList);
604 			}
605 		}
606 		
607 		List<AccrualCategory> activeAccrCatList = new ArrayList<AccrualCategory>();
608 		List<AccrualCategory> inactiveAccrCatList = new ArrayList<AccrualCategory>();
609 		for(String lpString : lpStringSet) {
610 			List<AccrualCategory> aList = TkServiceLocator.getAccrualCategoryService().getActiveLeaveAccrualCategoriesForLeavePlan(lpString, endDate);
611 			if(CollectionUtils.isNotEmpty(aList)) {
612 				activeAccrCatList.addAll(aList);
613 			}
614 			
615 			aList = TkServiceLocator.getAccrualCategoryService().getInActiveLeaveAccrualCategoriesForLeavePlan(lpString, endDate);
616 			if(CollectionUtils.isNotEmpty(aList)) {
617 				inactiveAccrCatList.addAll(aList);
618 			}
619 		}
620 		
621 		List<AccrualCategoryRule> activeRuleList = new ArrayList<AccrualCategoryRule>();
622 		List<AccrualCategoryRule> inactiveRuleList = new ArrayList<AccrualCategoryRule>();
623 		for(AccrualCategory ac : activeAccrCatList) {
624 			List<AccrualCategoryRule> aRuleList = TkServiceLocator.getAccrualCategoryRuleService().getActiveRulesForAccrualCategoryId(ac.getLmAccrualCategoryId(), endDate);
625 			activeRuleList.addAll(aRuleList);
626 			
627 			aRuleList = TkServiceLocator.getAccrualCategoryRuleService().getInActiveRulesForAccrualCategoryId(ac.getLmAccrualCategoryId(), endDate);
628 			inactiveRuleList.addAll(aRuleList);
629 		}
630 		
631 		List<LeaveCalendarDocumentHeader> lcDocList = TkServiceLocator.getLeaveCalendarDocumentHeaderService().getAllDocumentHeadersInRangeForPricipalId(principalId, startDate, endDate);
632 		
633 		BigDecimal previousFte = null;
634 		List<Job> jobs = new ArrayList<Job>();
635 		
636 	    while (!gc.getTime().after(endDate)) {
637 	    	RateRange rateRange = new RateRange();
638 	    	java.util.Date currentDate = gc.getTime();
639 	    	
640 	    	jobs = this.getJobsForDate(activeJobs, inactiveJobs, currentDate);
641 	    	if(jobs.isEmpty()) {	// no jobs found for this day
642 	    		gc.add(Calendar.DATE, 1);
643 	    		continue;
644 	    	}
645 			rateRange.setJobs(jobs);
646 			
647 			// detect if there's a status change
648 			BigDecimal fteSum = TkServiceLocator.getJobService().getFteSumForJobs(jobs);
649 			rateRange.setAccrualRatePercentageModifier(fteSum);
650 			BigDecimal standardHours = TkServiceLocator.getJobService().getStandardHoursSumForJobs(jobs);
651 			rateRange.setStandardHours(standardHours);
652 			
653 			if(previousFte != null && !previousFte.equals(fteSum)) {
654 				rateRange.setStatusChanged(true);
655 				rrAggregate.setRateRangeChanged(true);
656 			}
657 			previousFte = fteSum;
658 			
659 			// figure out the PrincipalHRAttributes for this day
660 			PrincipalHRAttributes phra = this.getPrincipalHrAttributesForDate(phaList, currentDate);
661 			rateRange.setPrincipalHRAttributes(phra);
662 			
663 			if(rateRange.getPrincipalHRAttributes() != null) {
664 				// figure out if there's an end principalHrAttributes for the initial principalHRAttributes
665 				PrincipalHRAttributes endPhra = this.getInactivePrincipalHrAttributesForDate(inactivePhaList, rateRange.getPrincipalHRAttributes().getEffectiveDate(), currentDate);
666 				rateRange.setEndPrincipalHRAttributes(endPhra);
667 			}
668 			
669 			// get leave plan for this day
670 			if(rateRange.getPrincipalHRAttributes()!= null) {				
671 				rateRange.setLeavePlan(this.getLeavePlanForDate(activeLpList, inactiveLpList, rateRange.getPrincipalHRAttributes().getLeavePlan(), currentDate));
672 			}
673 			
674 			if(rateRange.getLeavePlan() != null) {
675 				// get accrual category list for this day
676 				List<AccrualCategory> acsForDay = this.getAccrualCategoriesForDate(activeAccrCatList, inactiveAccrCatList, rateRange.getLeavePlan().getLeavePlan(), currentDate);
677 				rateRange.setAcList(acsForDay);
678 				
679 				// get System scheduled time off for this day
680 				for(SystemScheduledTimeOff ssto : sstoList) {
681 					if(TKUtils.removeTime(ssto.getAccruedDate()).equals(TKUtils.removeTime(currentDate) )
682 							&& ssto.getLeavePlan().equals(rateRange.getLeavePlan().getLeavePlan())) {
683 						// if there exists a ssto accrualed leave block with this ssto id, it means the ssto hours has been banked or transferred by the employee
684 						// this logic depends on the deactivateOldAccruals() runs before buildRateRangeAggregate()
685 						// because deactivateOldAccruals() removes accrued ssto leave blocks unless they are banked/transferred
686 						List<LeaveBlock> sstoLbList = TkServiceLocator.getLeaveBlockService().getSSTOLeaveBlocks(principalId, ssto.getLmSystemScheduledTimeOffId(), ssto.getAccruedDate());
687 						if(CollectionUtils.isEmpty(sstoLbList)) {
688 							rateRange.setSysScheTimeOff(ssto);
689 						}
690 					}	
691 				}
692 			}
693 			// set accrual category rules for the day
694 			if(CollectionUtils.isNotEmpty(rateRange.getAcList())) {
695 				List<AccrualCategoryRule> rulesForDay = new ArrayList<AccrualCategoryRule>();
696 				for(AccrualCategory ac : rateRange.getAcList()) {
697 					rulesForDay.addAll(this.getAccrualCategoryRulesForDate
698 											(activeRuleList, ac.getLmAccrualCategoryId(), currentDate, rateRange.getPrincipalHRAttributes().getServiceDate()));
699 				}
700 				rateRange.setAcRuleList(rulesForDay);
701 		    	
702 			}
703 			
704 			DateTime beginInterval = new DateTime(gc.getTime());
705 			gc.add(Calendar.DATE, 1);
706 			DateTime endInterval = new DateTime(gc.getTime());
707 			Interval range = new Interval(beginInterval, endInterval);
708 			rateRange.setRange(range);
709 			// assign leave document id to range if there is an existing leave doc for currentDate.
710 			// The doc Id will be assigned to leave blocks created at this rate range
711 			rateRange.setLeaveCalendarDocumentId(this.getLeaveDocumentForDate(lcDocList, currentDate));
712 			rateRangeList.add(rateRange);	       
713 	    }
714 		rrAggregate.setRateRanges(rateRangeList);
715 		rrAggregate.setCurrentRate(null);
716 		return rrAggregate;
717 	}
718 	
719 	private String getLeaveDocumentForDate(List<LeaveCalendarDocumentHeader> lcDocList, java.util.Date currentDate) {
720 		for(LeaveCalendarDocumentHeader lcdh : lcDocList) {
721 			if(!lcdh.getBeginDate().after(currentDate) && lcdh.getEndDate().after(currentDate)) {
722 				return lcdh.getDocumentId();
723 			}
724 		}
725 		return "";
726 	}
727 		
728 	public List<Job> getJobsForDate(List<Job> activeJobs, List<Job> inactiveJobs, java.util.Date currentDate) {
729 		List<Job> jobs = new ArrayList<Job>();
730     	for(Job aJob : activeJobs) {
731     		if(!aJob.getEffectiveDate().after(currentDate)) {
732     			jobs.add(aJob);
733     		}
734     	}
735     	if(CollectionUtils.isNotEmpty(jobs)) {
736 	    	List<Job> tempList = new ArrayList<Job>();
737 	    	tempList.addAll(jobs);
738 	    	for(Job aJob : tempList) {
739 	    		for(Job inactiveJob : inactiveJobs) {
740 	    			if(inactiveJob.getJobNumber().equals(aJob.getJobNumber())
741 	    				&& inactiveJob.getEffectiveDate().after(aJob.getEffectiveDate())
742 	    				&& !inactiveJob.getEffectiveDate().after(currentDate)) {
743 	    					// remove inactive job from list
744 	    					jobs.remove(aJob);
745 	    			}
746 	    		}
747 	    	}
748     	}
749     	return jobs;
750 	}
751 	
752 	public PrincipalHRAttributes getPrincipalHrAttributesForDate(List<PrincipalHRAttributes> activeList, java.util.Date currentDate) {
753 		List<PrincipalHRAttributes> phasForDay = new ArrayList<PrincipalHRAttributes>();
754 		for(PrincipalHRAttributes pha : activeList) {
755 			if(pha != null && pha.getEffectiveDate() != null && pha.getServiceDate() != null
756 					&& !pha.getEffectiveDate().after(currentDate) && !pha.getServiceDate().after(currentDate)) {
757     			phasForDay.add(pha);
758     		}
759 		}
760 		if(CollectionUtils.isNotEmpty(phasForDay)) {
761 			PrincipalHRAttributes pha = phasForDay.get(0);
762 			int indexOfMaxEffDt = 0;
763 			if(phasForDay.size() > 1) {
764 				for(int i = 1; i < phasForDay.size(); i++) {
765 					if( (phasForDay.get(i).getEffectiveDate().after(phasForDay.get(indexOfMaxEffDt).getEffectiveDate()))
766 							||(phasForDay.get(i).getEffectiveDate().equals(phasForDay.get(indexOfMaxEffDt).getEffectiveDate())
767 									&& phasForDay.get(i).getTimestamp().after(phasForDay.get(indexOfMaxEffDt).getTimestamp()))) {
768 						indexOfMaxEffDt = i;
769 					}
770 				}
771 				pha = phasForDay.get(indexOfMaxEffDt);
772 			}
773 			return pha;
774 		}
775 		return null;
776 	}
777 	
778 	public PrincipalHRAttributes getInactivePrincipalHrAttributesForDate(List<PrincipalHRAttributes> inactiveList, java.util.Date activeDate, java.util.Date currentDate) {
779 		List<PrincipalHRAttributes> inactivePhasForDay = new ArrayList<PrincipalHRAttributes>();
780 		for(PrincipalHRAttributes pha : inactiveList) {
781 			if( pha.getEffectiveDate().after(activeDate) && !pha.getServiceDate().after(currentDate)) {
782 				inactivePhasForDay.add(pha);
783     		}
784 		}
785 		if(CollectionUtils.isNotEmpty(inactivePhasForDay)) {
786 			PrincipalHRAttributes pha = inactivePhasForDay.get(0);
787 			int indexOfMaxEffDt = 0;
788 			if(inactivePhasForDay.size() > 1) {
789 				for(int i = 1; i < inactivePhasForDay.size(); i++) {
790 					if( (inactivePhasForDay.get(i).getEffectiveDate().after(inactivePhasForDay.get(indexOfMaxEffDt).getEffectiveDate()))
791 							||(inactivePhasForDay.get(i).getEffectiveDate().equals(inactivePhasForDay.get(indexOfMaxEffDt).getEffectiveDate())
792 									&& inactivePhasForDay.get(i).getTimestamp().after(inactivePhasForDay.get(indexOfMaxEffDt).getTimestamp()))) {
793 						indexOfMaxEffDt = i;
794 					}
795 				}
796 				pha = inactivePhasForDay.get(indexOfMaxEffDt);
797 			}
798 			return pha;
799 		}
800 		return null;
801 	}
802 	
803 	public LeavePlan getLeavePlanForDate(List<LeavePlan> activeLpList, List<LeavePlan> inactiveLpList, String leavePlan, java.util.Date currentDate) {
804 		List<LeavePlan> lpsForDay = new ArrayList<LeavePlan>();
805 		for(LeavePlan lp : activeLpList) {
806 			if(lp.getLeavePlan().equals(leavePlan) && !lp.getEffectiveDate().after(currentDate)) {
807 				lpsForDay.add(lp);
808 			}
809 		}
810 		List<LeavePlan> aList = new ArrayList<LeavePlan>();
811 		aList.addAll(lpsForDay);
812     	for(LeavePlan lp : aList) {
813     		for(LeavePlan inactiveLp : inactiveLpList) {
814     			if(inactiveLp.getLeavePlan().equals(lp.getLeavePlan())
815     				&& inactiveLp.getEffectiveDate().after(lp.getEffectiveDate())
816     				&& !inactiveLp.getEffectiveDate().after(currentDate)) {
817     					// remove inactive leave plan from list
818     					lpsForDay.remove(lp);
819     			}
820     		}
821     	}
822 		if(CollectionUtils.isNotEmpty(lpsForDay)) {
823 			LeavePlan aLp = lpsForDay.get(0);
824 			int indexOfMaxEffDt = 0;
825 			if(lpsForDay.size() > 1) {
826 				for(int i = 1; i < lpsForDay.size(); i++) {
827 					if( (lpsForDay.get(i).getEffectiveDate().after(lpsForDay.get(indexOfMaxEffDt).getEffectiveDate()))
828 							||(lpsForDay.get(i).getEffectiveDate().equals(lpsForDay.get(indexOfMaxEffDt).getEffectiveDate())
829 									&& lpsForDay.get(i).getTimestamp().after(lpsForDay.get(indexOfMaxEffDt).getTimestamp()))) {
830 						indexOfMaxEffDt = i;
831 					}
832 				}
833 				aLp = lpsForDay.get(indexOfMaxEffDt);
834 			}
835 			return aLp;
836 		}
837 		return null;
838 	}
839 	
840 	public List<AccrualCategory> getAccrualCategoriesForDate(List<AccrualCategory> activeAccrCatList, List<AccrualCategory> inactiveAccrCatList, String leavePlan, java.util.Date currentDate) {
841 		Set<AccrualCategory> aSet = new HashSet<AccrualCategory>();
842 		for(AccrualCategory ac : activeAccrCatList) {
843 			if(ac.getLeavePlan().equals(leavePlan) && !ac.getEffectiveDate().after(currentDate)) {
844 				aSet.add(ac);
845 			}
846 		}
847 		List<AccrualCategory> list1 = new ArrayList<AccrualCategory>();
848 		list1.addAll(aSet);
849     	for(AccrualCategory ac : list1) {
850     		for(AccrualCategory inactiveAc : inactiveAccrCatList) {
851     			if(inactiveAc.getAccrualCategory().equals(ac.getAccrualCategory())
852     				&& inactiveAc.getEffectiveDate().after(ac.getEffectiveDate())
853     				&& !inactiveAc.getEffectiveDate().after(currentDate)) {
854     					// remove inactive accrual category from list
855     				aSet.remove(ac);
856     			}
857     		}
858     	}
859     	List<AccrualCategory> acsForDay = new ArrayList<AccrualCategory>();
860     	acsForDay.addAll(aSet);
861     	return acsForDay;    	
862 	}
863 	
864 	@Override
865 	public boolean isEmpoyeementFutureStatusChanged(String principalId, Date startDate, Date endDate) {
866 		Date currentDate = TKUtils.getCurrentDate();
867 		if(endDate.after(currentDate)) {
868 			RateRangeAggregate rrAggregate = this.buildRateRangeAggregate(principalId, startDate, endDate);
869 			if(rrAggregate.isRateRangeChanged()) {
870 				return true;
871 			}
872 		}
873 		return false;
874 	}
875 	
876 	@Override
877 	public void calculateFutureAccrualUsingPlanningMonth(String principalId, Date asOfDate) {
878 		PrincipalHRAttributes phra = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(principalId, asOfDate);
879 		if(phra != null) {
880 			// use the date from pay period to get the leave plan
881 			LeavePlan lp = TkServiceLocator.getLeavePlanService().getLeavePlan(phra.getLeavePlan(), asOfDate);  
882 			if(lp != null && StringUtils.isNotEmpty(lp.getPlanningMonths())) {
883 				Calendar aCal = Calendar.getInstance();
884 				// go back a year 
885 				aCal.setTime(asOfDate);
886 				aCal.add(Calendar.YEAR, -1);
887 				if(aCal.getActualMaximum(Calendar.DAY_OF_MONTH) < aCal.get(Calendar.DATE)) {
888 					aCal.set(Calendar.DATE, aCal.getActualMaximum(Calendar.DAY_OF_MONTH));
889 				}
890 				Date startDate = new java.sql.Date(aCal.getTime().getTime());
891 				// go forward using planning months
892 				aCal.setTime(asOfDate);
893 				aCal.add(Calendar.MONTH, Integer.parseInt(lp.getPlanningMonths()));
894 				// max days in months differ, if the date is bigger than the max day, set it to the max day of the month
895 				if(aCal.getActualMaximum(Calendar.DAY_OF_MONTH) < aCal.get(Calendar.DATE)) {
896 					aCal.set(Calendar.DATE, aCal.getActualMaximum(Calendar.DAY_OF_MONTH));
897 				}
898 				Date endDate = new java.sql.Date(aCal.getTime().getTime());
899 				TkServiceLocator.getLeaveAccrualService().runAccrual(principalId, startDate, endDate, true);
900 			}
901 		}
902 	}
903 	
904 	private boolean minimumPercentageReachedForPayPeriod(BigDecimal min, String earnInterval, int workDays, Date intervalDate, String payCalName,  Map<String, List<CalendarEntries>> aMap) {
905 		if(min == null || min.compareTo(BigDecimal.ZERO) == 0) {
906 			return true;
907 		}
908 		int daysInInterval = this.getWorkDaysInInterval(intervalDate, earnInterval, payCalName, aMap);
909 		if(daysInInterval == 0) {
910 			return true;
911 		}
912 		BigDecimal actualPercentage =  new BigDecimal(workDays).divide(new BigDecimal(daysInInterval), 2, BigDecimal.ROUND_HALF_EVEN);
913 		if(actualPercentage.compareTo(min) >= 0) {
914 			return true;
915 		}
916 		
917 		return false;	
918 	}
919 
920 	private java.util.Date getPrevIntervalDate(Date aDate, String earnInterval, String payCalName,  Map<String, List<CalendarEntries>> aMap) {
921 		if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) {
922 			return this.getPrevPayCalIntervalDate(aDate, earnInterval, payCalName, aMap);
923 		} else {
924 			return this.getPreviousAccrualIntervalDate(earnInterval, aDate);
925 		}
926 	}
927 	
928 	@Override
929 	public java.util.Date getPreviousAccrualIntervalDate(String earnInterval, Date aDate) {
930 		Calendar aCal = Calendar.getInstance();
931 		aCal.setTime(aDate);
932 
933 		if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.DAILY)) {
934 			aCal.add(Calendar.DAY_OF_YEAR, -1);
935 		} else if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.WEEKLY)) {
936 			aCal.add(Calendar.WEEK_OF_YEAR, -1);
937 			aCal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);	// set to the Saturday of previous week
938 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.SEMI_MONTHLY)) {
939 			aCal.add(Calendar.DAY_OF_YEAR, -15);
940 			if(aCal.get(Calendar.DAY_OF_MONTH) <=15) {
941 				aCal.set(Calendar.DAY_OF_MONTH, 15);
942 			} else {
943 				aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH));
944 			}
945 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.MONTHLY)) {
946 			aCal.add(Calendar.MONTH, -1);
947 			aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH));
948 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.YEARLY)) {
949 			aCal.add(Calendar.YEAR, -1);
950 			aCal.set(Calendar.DAY_OF_YEAR, aCal.getActualMaximum(Calendar.DAY_OF_YEAR));
951 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.NO_ACCRUAL)) {
952 			// no change to calendar
953 		} 
954 		return aCal.getTime();
955 	}
956 	
957 	private java.util.Date getPrevPayCalIntervalDate(java.util.Date aDate, String earnInterval, String payCalName,  Map<String, List<CalendarEntries>> aMap) {
958 		if(StringUtils.isNotEmpty(payCalName) 
959 				&& !aMap.isEmpty()
960 				&& earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) {	// only used for ac earn interval == pay calendar
961 			List<CalendarEntries> entryList = aMap.get(payCalName);
962 			if(CollectionUtils.isNotEmpty(entryList)) {
963 				for(CalendarEntries anEntry : entryList) {
964 					// endPeriodDate of calendar entry is the beginning hour of the next day, so we need to substract one day from it to get the real end date
965 					java.util.Date endDate = TKUtils.addDates(anEntry.getEndPeriodDate(), -1);
966 					if(anEntry.getBeginPeriodDate().compareTo(aDate) <= 0 && endDate.compareTo(aDate) >= 0) {
967 						// the day before the beginning date of the cal entry that contains the passed in date is the endDate of previous calendar entry
968 						java.util.Date prevIntvDate = TKUtils.addDates(anEntry.getBeginPeriodDate(), -1);
969 						return prevIntvDate;
970 					}
971 				}
972 			}
973 		}
974 		return aDate;
975 	}
976 	
977 	private java.util.Date getNextIntervalDate(Date aDate, String earnInterval, String payCalName,  Map<String, List<CalendarEntries>> aMap) {
978 		if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) {
979 			return this.getNextPayCalIntervalDate(aDate, earnInterval, payCalName, aMap);
980 		} else {
981 			return this.getNextAccrualIntervalDate(earnInterval, aDate);
982 		}
983 	}
984 	
985 	private java.util.Date getNextPayCalIntervalDate(Date aDate, String earnInterval, String payCalName,  Map<String, List<CalendarEntries>> aMap) {
986 		if(StringUtils.isNotEmpty(payCalName) 
987 				&& !aMap.isEmpty()
988 				&& earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) {	// only used for ac earn interval == pay calendar
989 			List<CalendarEntries> entryList = aMap.get(payCalName);
990 			if(CollectionUtils.isNotEmpty(entryList)) {
991 				for(CalendarEntries anEntry : entryList) {
992 					// endPeriodDate of calendar entry is the beginning hour of the next day, so we need to substract one day from it to get the real end date
993 					java.util.Date endDate = TKUtils.addDates(anEntry.getEndPeriodDate(), -1);
994 					if(anEntry.getBeginPeriodDate().compareTo(aDate) <= 0 && endDate.compareTo(aDate) >= 0) {
995 						// the endDate of the cal entry that contains the passed in date is the next pay calendar interval date
996 						return endDate;
997 					}
998 				}
999 			}
1000 		}
1001 		return aDate;
1002 	}
1003 	
1004 	@Override
1005 	public java.util.Date getNextAccrualIntervalDate(String earnInterval, Date aDate) {
1006 		Calendar aCal = Calendar.getInstance();
1007 		aCal.setTime(aDate);
1008 		
1009 		if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.DAILY)) {
1010 			// no change to calendar
1011 		} else if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.WEEKLY)) {
1012 			if(aCal.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY) {
1013 				aCal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);	// set to the Saturday of previous week
1014 			} else {
1015 				aCal.add(Calendar.WEEK_OF_YEAR, 1);
1016 			}
1017 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.SEMI_MONTHLY)) {
1018 			if(aCal.get(Calendar.DAY_OF_MONTH) <=15) {
1019 				aCal.set(Calendar.DAY_OF_MONTH, 15);
1020 			} else {
1021 				aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH));
1022 			}
1023 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.MONTHLY)) {
1024 			aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH));
1025 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.YEARLY)) {
1026 			aCal.set(Calendar.DAY_OF_YEAR, aCal.getActualMaximum(Calendar.DAY_OF_YEAR));
1027 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.NO_ACCRUAL)) {
1028 			// no change to calendar
1029 		} 
1030 		return aCal.getTime();
1031 	}
1032 
1033 	private int getWorkDaysInInterval(Date aDate, String earnInterval, String payCalName,  Map<String, List<CalendarEntries>> aMap) {
1034 		if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) {
1035 			return this.getWorkDaysInPayCalInterval(aDate, earnInterval, payCalName, aMap);
1036 		} else {
1037 			return this.getWorkDaysInAccrualInterval(earnInterval, aDate);
1038 		}
1039 	}
1040 	
1041 	private int getWorkDaysInPayCalInterval(Date aDate, String earnInterval, String payCalName,  Map<String, List<CalendarEntries>> aMap) {
1042 		if(StringUtils.isNotEmpty(payCalName) 
1043 				&& !aMap.isEmpty()
1044 				&& earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) {	// only used for ac earn interval == pay calendar
1045 			List<CalendarEntries> entryList = aMap.get(payCalName);
1046 			if(CollectionUtils.isNotEmpty(entryList)) {
1047 				for(CalendarEntries anEntry : entryList) {
1048 					// endPeriodDate of calendar entry is the beginning hour of the next day, so we need to substract one day from it to get the real end date
1049 					java.util.Date endDate = TKUtils.addDates(anEntry.getEndPeriodDate(), -1);
1050 					if(anEntry.getBeginPeriodDate().compareTo(aDate) <= 0 && endDate.compareTo(aDate) >= 0) {
1051 						return TKUtils.getWorkDays(anEntry.getBeginPeriodDate(), endDate);
1052 					}
1053 				}
1054 			}
1055 		}
1056 		return 0;
1057 	}
1058 	
1059 	@Override
1060 	public int getWorkDaysInAccrualInterval(String earnInterval, Date aDate) {
1061 		Calendar aCal = Calendar.getInstance();
1062 		aCal.setTime(aDate);
1063 		
1064 		if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.DAILY)) {
1065 			return 1;
1066 		} else if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.WEEKLY)) {
1067 			return 5;	
1068 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.SEMI_MONTHLY)) {
1069 			if(aCal.get(Calendar.DAY_OF_MONTH) <= 15) {
1070 				aCal.set(Calendar.DAY_OF_MONTH, 1);
1071 				java.util.Date start = aCal.getTime();
1072 				aCal.set(Calendar.DAY_OF_MONTH, 15);
1073 				java.util.Date end = aCal.getTime();
1074 				return TKUtils.getWorkDays(start, end);
1075 			} else {
1076 				aCal.set(Calendar.DAY_OF_MONTH, 16);
1077 				java.util.Date start = aCal.getTime();
1078 				aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH));
1079 				java.util.Date end = aCal.getTime();
1080 				return TKUtils.getWorkDays(start, end);
1081 			}
1082 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.MONTHLY)) {
1083 			aCal.set(Calendar.DAY_OF_MONTH, 1);
1084 			java.util.Date start = aCal.getTime();
1085 			aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH));
1086 			java.util.Date end = aCal.getTime();
1087 			return TKUtils.getWorkDays(start, end);
1088 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.YEARLY)) {
1089 			aCal.set(Calendar.DAY_OF_YEAR, 1);
1090 			java.util.Date start = aCal.getTime();
1091 			aCal.set(Calendar.DAY_OF_YEAR, aCal.getActualMaximum(Calendar.DAY_OF_YEAR));
1092 			java.util.Date end = aCal.getTime();
1093 			return TKUtils.getWorkDays(start, end);
1094 		} else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.NO_ACCRUAL)) {
1095 			return 0;
1096 		}		
1097 		return 0;
1098 	}
1099 	
1100 	public java.util.Date getRuleStartDate(String earnInterval, Date serviceDate, Long startAcc) {
1101 		Calendar aCal = Calendar.getInstance();
1102 		aCal.setTime(serviceDate);
1103 		String intervalValue = TkConstants.SERVICE_UNIT_OF_TIME.get(earnInterval);
1104 		int startInt = startAcc.intValue();
1105 		
1106 		if (intervalValue.equals("Months")) {
1107 			aCal.add(Calendar.MONTH, startInt);
1108 			if(aCal.get(Calendar.DAY_OF_MONTH) > aCal.getActualMaximum(Calendar.DAY_OF_MONTH)) {
1109 				aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH));
1110 			}
1111 		} else if (intervalValue.equals("Years")) {
1112 			aCal.set(Calendar.YEAR, startInt);
1113 		}else {
1114 			// no change to calendar
1115 		}
1116 		return aCal.getTime();
1117 	}
1118 	
1119 	public boolean getProrationFlag(String proration) {
1120 		if(proration == null) {
1121 			return true;
1122 		}
1123 		return proration.equals("Y") ? true : false;
1124 	}
1125 	
1126 	@Override
1127 	public boolean statusChangedSinceLastRun(String principalId) {
1128 		PrincipalAccrualRan par = TkServiceLocator.getPrincipalAccrualRanService().getLastPrincipalAccrualRan(principalId);
1129 		if(par == null) {
1130 			return true;
1131 		}
1132 		Job aJob = TkServiceLocator.getJobService().getMaxTimestampJob(principalId);
1133 		
1134 		if(aJob != null && aJob.getTimestamp().after(par.getLastRanTs())) {
1135 			return true;
1136 		}
1137 		
1138 		Assignment anAssign = TkServiceLocator.getAssignmentService().getMaxTimestampAssignment(principalId);
1139 		if(anAssign != null && anAssign.getTimestamp().after(par.getLastRanTs())) {
1140 			return true;
1141 		}
1142 		
1143 		PrincipalHRAttributes pha = TkServiceLocator.getPrincipalHRAttributeService().getMaxTimeStampPrincipalHRAttributes(principalId);
1144 		if(pha != null && pha.getTimestamp().after(par.getLastRanTs())) {
1145 			return true;
1146 		}
1147 		// if there are leave blocks created for earn codes with eligible-for-accrual = no since the last accrual run, it should trigger recalculation 
1148 		List<LeaveBlock> lbList = TkServiceLocator.getLeaveBlockService().getABELeaveBlocksSinceTime(principalId, par.getLastRanTs());
1149 		if(CollectionUtils.isNotEmpty(lbList)) {
1150 			return true;
1151 		}		
1152 		return false;
1153 	}
1154 	
1155     public List<AccrualCategoryRule> getAccrualCategoryRulesForDate(List<AccrualCategoryRule> acrList, String accrualCategoryId, java.util.Date currentDate, java.util.Date serviceDate) {
1156     	Calendar startCal = new GregorianCalendar();
1157     	Calendar endCal = new GregorianCalendar();
1158     	List<AccrualCategoryRule> aList = new ArrayList<AccrualCategoryRule>();
1159     	if(CollectionUtils.isNotEmpty(acrList)) {
1160 	    	for(AccrualCategoryRule acr : acrList) {
1161 	    		if(acr.getLmAccrualCategoryId().equals(accrualCategoryId)) {
1162 		    		String uot = acr.getServiceUnitOfTime();
1163 		    		int startTime = acr.getStart().intValue();
1164 					int endTime = acr.getEnd().intValue();
1165 					
1166 					startCal.setTime(serviceDate);
1167 					endCal.setTime(serviceDate);
1168 		    		if(uot.equals("M")) {		// monthly
1169 		    			startCal.add(Calendar.MONTH, startTime);
1170 		    			endCal.add(Calendar.MONTH, endTime);
1171 		    			endCal.add(Calendar.DATE, -1);
1172 		    		} else if(uot.endsWith("Y")) { // yearly
1173 		    			startCal.add(Calendar.YEAR, startTime);
1174 		    			endCal.add(Calendar.YEAR, endTime);
1175 		    			endCal.add(Calendar.DATE, -1);
1176 		    		}
1177 		    		
1178 		    		// max days in months differ, if the date is bigger than the max day, set it to the max day of the month
1179 					if(startCal.getActualMaximum(Calendar.DAY_OF_MONTH) < startCal.get(Calendar.DATE)) {
1180 						startCal.set(Calendar.DATE, startCal.getActualMaximum(Calendar.DAY_OF_MONTH));
1181 					}
1182 					if(endCal.getActualMaximum(Calendar.DAY_OF_MONTH) < endCal.get(Calendar.DATE)) {
1183 						endCal.set(Calendar.DATE, endCal.getActualMaximum(Calendar.DAY_OF_MONTH));
1184 					}
1185 		    		
1186 		    		if(TKUtils.removeTime(currentDate).compareTo(TKUtils.removeTime(startCal.getTime())) >= 0 
1187 		    				&& TKUtils.removeTime(currentDate).compareTo(TKUtils.removeTime(endCal.getTime())) <=0 ) {
1188 		    			aList.add(acr);
1189 		    		}
1190 	    		}
1191 	    	}
1192     	}
1193     	return aList;
1194 	}
1195     
1196     public AccrualCategoryRule getRuleForAccrualCategory(List<AccrualCategoryRule> acrList, AccrualCategory ac) {
1197     	if(CollectionUtils.isNotEmpty(acrList)) {
1198 	    	for(AccrualCategoryRule acr : acrList) {
1199 	    		if(acr.getLmAccrualCategoryId().equals(ac.getLmAccrualCategoryId())) {
1200 	    			return acr;
1201 	    		}
1202 	    	}
1203 	    }
1204     	return null;
1205     }
1206 }