001    /**
002     * Copyright 2004-2013 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.hr.lm.leavepayout.service;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.commons.lang.time.DateUtils;
020    import org.joda.time.DateTime;
021    import org.joda.time.Interval;
022    import org.kuali.hr.lm.LMConstants;
023    import org.kuali.hr.lm.accrual.AccrualCategory;
024    import org.kuali.hr.lm.accrual.AccrualCategoryRule;
025    import org.kuali.hr.lm.leavepayout.LeavePayout;
026    import org.kuali.hr.lm.employeeoverride.EmployeeOverride;
027    import org.kuali.hr.lm.leaveSummary.LeaveSummary;
028    import org.kuali.hr.lm.leaveSummary.LeaveSummaryRow;
029    import org.kuali.hr.lm.leaveblock.LeaveBlock;
030    import org.kuali.hr.lm.leaveblock.LeaveBlockHistory;
031    import org.kuali.hr.lm.leavecalendar.LeaveCalendarDocument;
032    import org.kuali.hr.lm.leavepayout.LeavePayout;
033    import org.kuali.hr.lm.leavepayout.dao.LeavePayoutDao;
034    
035    import org.kuali.hr.lm.leaveplan.LeavePlan;
036    import org.kuali.hr.time.calendar.CalendarEntries;
037    import org.kuali.hr.time.earncode.EarnCode;
038    import org.kuali.hr.time.principal.PrincipalHRAttributes;
039    import org.kuali.hr.time.service.base.TkServiceLocator;
040    import org.kuali.hr.time.util.TKContext;
041    import org.kuali.hr.time.util.TKUtils;
042    import org.kuali.hr.time.util.TkConstants;
043    import org.kuali.rice.kew.api.exception.WorkflowException;
044    import org.kuali.rice.kim.api.identity.principal.EntityNamePrincipalName;
045    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
046    import org.kuali.rice.krad.maintenance.MaintenanceDocument;
047    import org.kuali.rice.krad.service.KRADServiceLocator;
048    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
049    import org.kuali.rice.krad.util.KRADConstants;
050    import org.kuali.rice.krad.util.ObjectUtils;
051    
052    import edu.emory.mathcs.backport.java.util.Collections;
053    
054    import java.math.BigDecimal;
055    import java.sql.Date;
056    import java.util.ArrayList;
057    import java.util.Calendar;
058    import java.util.Comparator;
059    import java.util.HashMap;
060    import java.util.HashSet;
061    import java.util.List;
062    import java.util.Map;
063    import java.util.Set;
064    
065    public class LeavePayoutServiceImpl implements LeavePayoutService {
066    
067        private LeavePayoutDao leavePayoutDao;
068    
069        @Override
070        public List<LeavePayout> getAllLeavePayoutsForPrincipalId(
071                String principalId) {
072            return leavePayoutDao.getAllLeavePayoutsForPrincipalId(principalId);
073        }
074    
075        @Override
076        public List<LeavePayout> getAllLeavePayoutsForPrincipalIdAsOfDate(
077                String principalId, Date effectiveDate) {
078            return leavePayoutDao.getAllLeavePayoutsForPrincipalIdAsOfDate(principalId,effectiveDate);
079        }
080    
081        @Override
082        public List<LeavePayout> getAllLeavePayoutsByEffectiveDate(
083                Date effectiveDate) {
084            return leavePayoutDao.getAllLeavePayoutsByEffectiveDate(effectiveDate);
085        }
086    
087        @Override
088        public LeavePayout getLeavePayoutById(String lmLeavePayoutId) {
089            return leavePayoutDao.getLeavePayoutById(lmLeavePayoutId);
090        }
091    
092        public LeavePayoutDao getLeavePayoutDao() {
093            return leavePayoutDao;
094        }
095    
096        public void setLeavePayoutDao(LeavePayoutDao leavePayoutDao) {
097            this.leavePayoutDao = leavePayoutDao;
098        }
099    
100            @Override
101            public LeavePayout initializePayout(String principalId,
102                            String accrualCategoryRule, BigDecimal accruedBalance,
103                            Date effectiveDate) {
104                    //Initially, principals may be allowed to edit the transfer amount when prompted to submit this balance transfer, however,
105                    //a base transfer amount together with a forfeited amount is calculated to bring the balance back to its limit in accordance
106                    //with transfer limits.
107                    LeavePayout leavePayout = null;
108                    AccrualCategoryRule accrualRule = TkServiceLocator.getAccrualCategoryRuleService().getAccrualCategoryRule(accrualCategoryRule);
109    
110                    if(ObjectUtils.isNotNull(accrualRule) && ObjectUtils.isNotNull(accruedBalance)) {
111                            leavePayout = new LeavePayout();
112                            //Leave summary is not a requirement, per se, but the information it contains is.
113                            //The only thing that is obtained from leave summary is the accrued balance of the leave summary row matching the
114                            //passed accrualCategoryRules accrual category.
115                            //These two objects are essential to balance transfers triggered when the employee submits their leave calendar for approval.
116                            //Neither of these objects should be null, otherwise this method could not have been called.
117                            AccrualCategory fromAccrualCategory = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(accrualRule.getLmAccrualCategoryId());
118                            BigDecimal fullTimeEngagement = TkServiceLocator.getJobService().getFteSumForAllActiveLeaveEligibleJobs(principalId, effectiveDate);
119                            
120                            // AccrualRule.maxBalance == null -> no balance limit. No balance limit -> no accrual triggered transfer / payout / lose.
121                            // execution point should not be here if max balance on accrualRule is null, unless there exists an employee override.
122                            BigDecimal maxBalance = accrualRule.getMaxBalance();
123                            BigDecimal adjustedMaxBalance = maxBalance.multiply(fullTimeEngagement).setScale(2);
124                            
125                            BigDecimal maxPayoutAmount = null;
126                            BigDecimal adjustedMaxPayoutAmount = null;
127                            if(ObjectUtils.isNotNull(accrualRule.getMaxPayoutAmount())) {
128                                    maxPayoutAmount = new BigDecimal(accrualRule.getMaxPayoutAmount());
129                                    adjustedMaxPayoutAmount = maxPayoutAmount.multiply(fullTimeEngagement).setScale(2);
130                            }
131                            else {
132                                    // no limit on transfer amount
133                                    maxPayoutAmount = new BigDecimal(Long.MAX_VALUE);
134                                    adjustedMaxPayoutAmount = maxPayoutAmount;
135                            }
136    
137                            BigDecimal maxCarryOver = null;
138                            BigDecimal adjustedMaxCarryOver = null;
139                            if(ObjectUtils.isNotNull(accrualRule.getMaxCarryOver())) {
140                                    maxCarryOver = new BigDecimal(accrualRule.getMaxCarryOver());
141                                    adjustedMaxCarryOver = maxCarryOver.multiply(fullTimeEngagement).setScale(2);
142                            }
143                            else {
144                                    //no limit to carry over.
145                                    maxCarryOver = new BigDecimal(Long.MAX_VALUE);
146                                    adjustedMaxCarryOver = maxCarryOver;
147                            }
148                            
149                            EmployeeOverride maxBalanceOverride = TkServiceLocator.getEmployeeOverrideService().getEmployeeOverride(principalId, fromAccrualCategory.getLeavePlan(), fromAccrualCategory.getAccrualCategory(), "MB", effectiveDate);
150                            EmployeeOverride maxPayoutAmountOverride = TkServiceLocator.getEmployeeOverrideService().getEmployeeOverride(principalId, fromAccrualCategory.getLeavePlan(), fromAccrualCategory.getAccrualCategory(), "MPA", effectiveDate);
151                            EmployeeOverride maxAnnualCarryOverOverride = TkServiceLocator.getEmployeeOverrideService().getEmployeeOverride(principalId, fromAccrualCategory.getLeavePlan(), fromAccrualCategory.getAccrualCategory(), "MAC", effectiveDate);
152                            //Do not pro-rate override values for FTE.
153                            if(maxBalanceOverride != null)
154                                    adjustedMaxBalance = new BigDecimal(maxBalanceOverride.getOverrideValue());
155                            if(maxPayoutAmountOverride != null)
156                                    adjustedMaxPayoutAmount = new BigDecimal(maxPayoutAmountOverride.getOverrideValue());
157                            if(maxAnnualCarryOverOverride != null)
158                                    adjustedMaxCarryOver = new BigDecimal(maxAnnualCarryOverOverride.getOverrideValue());
159                            
160                            
161                            BigDecimal transferAmount = accruedBalance.subtract(adjustedMaxBalance);
162                            if(transferAmount.compareTo(adjustedMaxPayoutAmount) > 0) {
163                                    //there's forfeiture.
164                                    //bring transfer amount down to the adjusted maximum transfer amount, and place excess in forfeiture.
165                                    //accruedBalance - adjustedMaxPayoutAmount - adjustedMaxBalance = forfeiture.
166                                    //transferAmount = accruedBalance - adjustedMaxBalance; forfeiture = transferAmount - adjustedMaxPayoutAmount.
167                                    BigDecimal forfeiture = transferAmount.subtract(adjustedMaxPayoutAmount).setScale(2);
168                                    forfeiture = forfeiture.stripTrailingZeros();
169                                    leavePayout.setForfeitedAmount(forfeiture);
170                                    leavePayout.setPayoutAmount(adjustedMaxPayoutAmount);
171                            }
172                            else {
173                                    leavePayout.setPayoutAmount(transferAmount);
174                                    leavePayout.setForfeitedAmount(BigDecimal.ZERO);
175                            }
176                                    
177                            assert(adjustedMaxBalance.compareTo(accruedBalance.subtract(leavePayout.getPayoutAmount().add(leavePayout.getForfeitedAmount()))) == 0);
178                            
179                            // Max Carry Over logic for Year End transfers.
180                            if(StringUtils.equals(accrualRule.getMaxBalanceActionFrequency(),LMConstants.MAX_BAL_ACTION_FREQ.YEAR_END)) {
181                                    if(ObjectUtils.isNotNull(maxCarryOver)) {
182                                            
183                                            if(ObjectUtils.isNull(adjustedMaxCarryOver))
184                                                    adjustedMaxCarryOver = maxCarryOver.multiply(fullTimeEngagement).setScale(2);
185                                            //otherwise, adjustedMaxCarryOver has an employee override value, which trumps accrual rule defined MAC.
186                                            //At this point, transfer amount and forfeiture have been set so that the new accrued balance will be the
187                                            //adjusted max balance, so this amount is used to check against carry over.
188                                            if(adjustedMaxBalance.compareTo(adjustedMaxCarryOver) > 0) {
189                                                    BigDecimal carryOverDiff = adjustedMaxBalance.subtract(adjustedMaxCarryOver);
190                                                    
191                                                    if(StringUtils.equals(accrualRule.getActionAtMaxBalance(),LMConstants.ACTION_AT_MAX_BAL.LOSE)){
192                                                            //add carry over excess to forfeiture.
193                                                            leavePayout.setForfeitedAmount(leavePayout.getForfeitedAmount().add(carryOverDiff));
194                                                    }
195                                                    else {
196                                                            //maximize the transfer amount.
197                                                            BigDecimal potentialPayoutAmount = leavePayout.getPayoutAmount().add(carryOverDiff);
198            
199                                                            //Can this entire amount be added to the transfer amount??
200                                                            if(potentialPayoutAmount.compareTo(adjustedMaxPayoutAmount) <= 0) {
201                                                                    //yes
202                                                                    leavePayout.setPayoutAmount(leavePayout.getPayoutAmount().add(carryOverDiff));
203                                                            }
204                                                            else {
205                                                                    //no
206                                                                    BigDecimal carryOverExcess = potentialPayoutAmount.subtract(adjustedMaxPayoutAmount);
207                                                                    //move excess to forfeiture
208                                                                    leavePayout.setForfeitedAmount(leavePayout.getForfeitedAmount().add(carryOverExcess));
209                                                                    //the remainder (if any) can be added to the transfer amount ( unless action is LOSE ).
210                                                                    leavePayout.setPayoutAmount(leavePayout.getPayoutAmount().add(carryOverDiff.subtract(carryOverExcess)));
211                                                                    
212                                                                    assert(adjustedMaxCarryOver.compareTo(accruedBalance.subtract(leavePayout.getPayoutAmount().add(leavePayout.getForfeitedAmount()))) == 0);
213                                                            }
214                                                    }
215                                            }
216                                            //otherwise, given balance will be at or under the max annual carry over.
217                                    }
218                            }
219                            
220                            leavePayout.setEffectiveDate(effectiveDate);
221                            leavePayout.setAccrualCategoryRule(accrualCategoryRule);
222                            leavePayout.setFromAccrualCategory(fromAccrualCategory.getAccrualCategory());
223                            leavePayout.setPrincipalId(principalId);
224                            leavePayout.setEarnCode(accrualRule.getMaxPayoutEarnCode());
225    
226                    }
227                    return leavePayout;
228            }
229    
230            @Override
231            public LeavePayout payout(LeavePayout leavePayout) {
232                    if(ObjectUtils.isNull(leavePayout))
233                            throw new RuntimeException("did not supply a valid LeavePayout object.");
234                    else {
235                            List<LeaveBlock> leaveBlocks = new ArrayList<LeaveBlock>();
236                            BigDecimal transferAmount = leavePayout.getPayoutAmount();
237                            LeaveBlock aLeaveBlock = null;
238                            
239                            if(ObjectUtils.isNotNull(transferAmount)) {
240                                    if(transferAmount.compareTo(BigDecimal.ZERO) > 0) {
241                    
242                                            aLeaveBlock = new LeaveBlock();
243                                            //Create a leave block that adds the adjusted transfer amount to the "transfer to" accrual category.
244                                            aLeaveBlock.setPrincipalId(leavePayout.getPrincipalId());
245                                            aLeaveBlock.setLeaveDate(leavePayout.getEffectiveDate());
246                                            aLeaveBlock.setEarnCode(leavePayout.getEarnCode());
247                                            aLeaveBlock.setAccrualCategory(leavePayout.getEarnCodeObj().getAccrualCategory());
248                                            aLeaveBlock.setDescription("Amount payed out");
249                                            aLeaveBlock.setLeaveAmount(leavePayout.getPayoutAmount());
250                                            aLeaveBlock.setAccrualGenerated(true);
251                                            aLeaveBlock.setTransactionDocId(leavePayout.getDocumentHeaderId());
252                                            aLeaveBlock.setLeaveBlockType(LMConstants.LEAVE_BLOCK_TYPE.LEAVE_PAYOUT);
253                                            aLeaveBlock.setRequestStatus(LMConstants.REQUEST_STATUS.REQUESTED);
254                                            aLeaveBlock.setDocumentId(leavePayout.getLeaveCalendarDocumentId());
255                                            aLeaveBlock.setBlockId(0L);
256    
257                                            //Want to store the newly created leave block id on this maintainable object
258                                            //when the status of the maintenance document encapsulating this maintainable changes
259                                            //the id will be used to fetch and update the leave block statuses.
260                                    aLeaveBlock = KRADServiceLocator.getBusinessObjectService().save(aLeaveBlock);
261    
262                                    leavePayout.setPayoutLeaveBlockId(aLeaveBlock.getLmLeaveBlockId());
263                                    // save history
264                                    LeaveBlockHistory lbh = new LeaveBlockHistory(aLeaveBlock);
265                                    lbh.setAction(LMConstants.ACTION.ADD);
266                                    TkServiceLocator.getLeaveBlockHistoryService().saveLeaveBlockHistory(lbh);
267                                            leaveBlocks.add(aLeaveBlock);
268                                            
269                                            //Create leave block that removes the correct transfer amount from the originating accrual category.
270                                            aLeaveBlock = new LeaveBlock();
271                                            aLeaveBlock.setPrincipalId(leavePayout.getPrincipalId());
272                                            aLeaveBlock.setLeaveDate(leavePayout.getEffectiveDate());
273                                            aLeaveBlock.setEarnCode(leavePayout.getFromAccrualCategoryObj().getEarnCode());
274                                            aLeaveBlock.setAccrualCategory(leavePayout.getFromAccrualCategory());
275                                            aLeaveBlock.setDescription("Payout amount");
276                                            aLeaveBlock.setLeaveAmount(leavePayout.getPayoutAmount().negate());
277                                            aLeaveBlock.setAccrualGenerated(true);
278                                            aLeaveBlock.setTransactionDocId(leavePayout.getDocumentHeaderId());
279                                            aLeaveBlock.setLeaveBlockType(LMConstants.LEAVE_BLOCK_TYPE.LEAVE_PAYOUT);
280                                            aLeaveBlock.setRequestStatus(LMConstants.REQUEST_STATUS.REQUESTED);
281                                            aLeaveBlock.setDocumentId(leavePayout.getLeaveCalendarDocumentId());
282                                            aLeaveBlock.setBlockId(0L);
283                                            
284                                            //Want to store the newly created leave block id on this maintainable object.
285                                            //when the status of the maintenance document encapsulating this maintainable changes
286                                            //the id will be used to fetch and update the leave block statuses.
287                                    aLeaveBlock = KRADServiceLocator.getBusinessObjectService().save(aLeaveBlock);
288    
289                                    leavePayout.setPayoutFromLeaveBlockId(aLeaveBlock.getLmLeaveBlockId());
290                                    // save history
291                                    lbh = new LeaveBlockHistory(aLeaveBlock);
292                                    lbh.setAction(LMConstants.ACTION.ADD);
293                                    TkServiceLocator.getLeaveBlockHistoryService().saveLeaveBlockHistory(lbh);
294                                            
295                                            leaveBlocks.add(aLeaveBlock);
296                                    }
297                            }
298                            
299                            BigDecimal forfeitedAmount = leavePayout.getForfeitedAmount();
300                            if(ObjectUtils.isNotNull(forfeitedAmount)) {
301                                    //Any amount forfeited must come out of the originating accrual category in order to bring balance back to max.
302                                    if(forfeitedAmount.compareTo(BigDecimal.ZERO) > 0) {
303                                            //for balance transfers with action = lose, transfer amount must be moved to forfeitedAmount
304                                            aLeaveBlock = new LeaveBlock();
305                                            aLeaveBlock.setPrincipalId(leavePayout.getPrincipalId());
306                                            aLeaveBlock.setLeaveDate(leavePayout.getEffectiveDate());
307                                            aLeaveBlock.setEarnCode(leavePayout.getFromAccrualCategoryObj().getEarnCode());
308                                            aLeaveBlock.setAccrualCategory(leavePayout.getFromAccrualCategory());
309                                            aLeaveBlock.setDescription("Forfeited payout amount");
310                                            aLeaveBlock.setLeaveAmount(forfeitedAmount.negate());
311                                            aLeaveBlock.setAccrualGenerated(true);
312                                            aLeaveBlock.setTransactionDocId(leavePayout.getDocumentHeaderId());
313                                            aLeaveBlock.setLeaveBlockType(LMConstants.LEAVE_BLOCK_TYPE.LEAVE_PAYOUT);
314                                            aLeaveBlock.setRequestStatus(LMConstants.REQUEST_STATUS.REQUESTED);
315                                            aLeaveBlock.setDocumentId(leavePayout.getLeaveCalendarDocumentId());
316                                            aLeaveBlock.setBlockId(0L);
317                                            
318                                            //Want to store the newly created leave block id on this maintainable object
319                                            //when the status of the maintenance document encapsulating this maintainable changes
320                                            //the id will be used to fetch and update the leave block statuses.
321                                    aLeaveBlock = KRADServiceLocator.getBusinessObjectService().save(aLeaveBlock);
322    
323                                    leavePayout.setForfeitedLeaveBlockId(aLeaveBlock.getLmLeaveBlockId());
324                                    // save history
325                                    LeaveBlockHistory lbh = new LeaveBlockHistory(aLeaveBlock);
326                                    lbh.setAction(LMConstants.ACTION.ADD);
327                                    TkServiceLocator.getLeaveBlockHistoryService().saveLeaveBlockHistory(lbh);
328                                            
329                                            leaveBlocks.add(aLeaveBlock);
330                                    }
331                            }
332    
333                            return leavePayout;
334                    }
335            }
336    
337            @Override
338            public void submitToWorkflow(LeavePayout leavePayout)
339                            throws WorkflowException {
340                    //leavePayout.setStatus(TkConstants.ROUTE_STATUS.ENROUTE);
341            EntityNamePrincipalName principalName = null;
342            if (leavePayout.getPrincipalId() != null) {
343                principalName = KimApiServiceLocator.getIdentityService().getDefaultNamesForPrincipalId(leavePayout.getPrincipalId());
344            }
345    
346                    MaintenanceDocument document = KRADServiceLocatorWeb.getMaintenanceDocumentService().setupNewMaintenanceDocument(LeavePayout.class.getName(),
347                                    "LeavePayoutDocumentType",KRADConstants.MAINTENANCE_NEW_ACTION);
348    
349            String personName = (principalName != null  && principalName.getDefaultName() != null) ? principalName.getDefaultName().getCompositeName() : StringUtils.EMPTY;
350            String date = TKUtils.formatDate(new java.sql.Date(leavePayout.getEffectiveDate().getTime()));
351            document.getDocumentHeader().setDocumentDescription(personName + " (" + leavePayout.getPrincipalId() + ")  - " + date);
352                    Map<String,String[]> params = new HashMap<String,String[]>();
353                    
354                    KRADServiceLocatorWeb.getMaintenanceDocumentService().setupMaintenanceObject(document, KRADConstants.MAINTENANCE_NEW_ACTION, params);
355                    LeavePayout lpObj = (LeavePayout) document.getNewMaintainableObject().getDataObject();
356                    
357                    lpObj.setAccrualCategoryRule(leavePayout.getAccrualCategoryRule());
358                    lpObj.setEffectiveDate(leavePayout.getEffectiveDate());
359                    lpObj.setForfeitedAmount(leavePayout.getForfeitedAmount());
360                    lpObj.setFromAccrualCategory(leavePayout.getFromAccrualCategory());
361                    lpObj.setPrincipalId(leavePayout.getPrincipalId());
362                    lpObj.setEarnCode(leavePayout.getEarnCode());
363                    lpObj.setPayoutAmount(leavePayout.getPayoutAmount());
364                    lpObj.setDocumentHeaderId(document.getDocumentHeader().getWorkflowDocument().getDocumentId());
365                    
366                    document.getNewMaintainableObject().setDataObject(lpObj);
367                    KRADServiceLocatorWeb.getDocumentService().saveDocument(document);
368                    document.getDocumentHeader().getWorkflowDocument().saveDocument("");
369    
370                    document.getDocumentHeader().getWorkflowDocument().route("");
371            }
372    
373            @Override
374            public List<LeavePayout> getLeavePayouts(String viewPrincipal,
375                            Date beginPeriodDate, Date endPeriodDate) {
376                    // TODO Auto-generated method stub
377                    return leavePayoutDao.getLeavePayouts(viewPrincipal, beginPeriodDate, endPeriodDate);
378            }
379    
380            @Override
381            public void saveOrUpdate(LeavePayout payout) {
382                    leavePayoutDao.saveOrUpdate(payout);
383            }
384    }