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