/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.module.tem.service.impl;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.fp.businessobject.TravelExpenseTypeCode;
import org.kuali.kfs.kns.service.DocumentHelperService;
import org.kuali.kfs.kns.util.KNSGlobalVariables;
import org.kuali.kfs.kns.web.struts.form.KualiForm;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.util.KRADConstants;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.tem.TemConstants;
import org.kuali.kfs.module.tem.TemConstants.AgencyStagingDataErrorCodes;
import org.kuali.kfs.module.tem.TemConstants.CreditCardStagingDataErrorCodes;
import org.kuali.kfs.module.tem.TemConstants.ExpenseImportTypes;
import org.kuali.kfs.module.tem.TemConstants.ExpenseTypeMetaCategory;
import org.kuali.kfs.module.tem.TemConstants.ReconciledCodes;
import org.kuali.kfs.module.tem.TemPropertyConstants;
import org.kuali.kfs.module.tem.businessobject.ActualExpense;
import org.kuali.kfs.module.tem.businessobject.AgencyStagingData;
import org.kuali.kfs.module.tem.businessobject.CreditCardAgency;
import org.kuali.kfs.module.tem.businessobject.CreditCardStagingData;
import org.kuali.kfs.module.tem.businessobject.ExpenseType;
import org.kuali.kfs.module.tem.businessobject.ExpenseTypeObjectCode;
import org.kuali.kfs.module.tem.businessobject.HistoricalTravelExpense;
import org.kuali.kfs.module.tem.businessobject.OtherExpense;
import org.kuali.kfs.module.tem.businessobject.TemExpense;
import org.kuali.kfs.module.tem.businessobject.TripAccountingInformation;
import org.kuali.kfs.module.tem.dataaccess.ExpenseTypeObjectCodeDao;
import org.kuali.kfs.module.tem.document.TravelDocument;
import org.kuali.kfs.module.tem.document.web.struts.TravelFormBase;
import org.kuali.kfs.module.tem.service.TemExpenseService;
import org.kuali.kfs.module.tem.service.TravelExpenseService;
import org.kuali.kfs.module.tem.service.TravelService;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.KimConstants;
import org.kuali.rice.kim.api.identity.Person;
import org.springframework.transaction.annotation.Transactional;

import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Service for handling travel expenses
 *
 * @see org.kuali.kfs.module.tem.document.validation.impl.AgencyStagingDataRule
 */
@Transactional
public class TravelExpenseServiceImpl implements TravelExpenseService {
    org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(TravelExpenseServiceImpl.class);

    protected BusinessObjectService businessObjectService;
    protected DateTimeService dateTimeService;
    protected ExpenseTypeObjectCodeDao expenseTypeObjectCodeDao;
    protected DocumentHelperService documentHelperService;
    protected TravelService travelService;

    @Override
    public ExpenseTypeObjectCode getExpenseType(String expense, String documentType, String tripType, String travelerType) {
        if (StringUtils.isBlank(expense)) {
            throw new IllegalArgumentException("Expense Type Code cannot be blank");
        }

        final Set<String> parentDocumentTypes = getTravelService().getParentDocumentTypeNames(documentType);
        List<ExpenseTypeObjectCode> expenseTypeObjectCodes = getExpenseTypeObjectCodeDao().findMatchingExpenseTypeObjectCodes(expense, parentDocumentTypes, tripType, travelerType);
        if (expenseTypeObjectCodes == null || expenseTypeObjectCodes.isEmpty()) {
            return null;
        }
        Collections.sort(expenseTypeObjectCodes, getExpenseTypeObjectCodeComparator());
        final ExpenseTypeObjectCode chosenExpenseTypeObjectCode = expenseTypeObjectCodes.get(0);
        if (LOG.isDebugEnabled()) {
            LOG.debug("I choose you, " + chosenExpenseTypeObjectCode.toString());
        }
        return chosenExpenseTypeObjectCode;
    }

    /**
     * @return the comparator that will sort expense type object codes, such that the top one will be selected as the "most" correct
     */
    protected Comparator<ExpenseTypeObjectCode> getExpenseTypeObjectCodeComparator() {
        return new ExpenseTypeObjectCodeComparatorByHierarchyLogic();
    }

    /**
     * This wise child knows how to sort ExpenseTypeObjectCode records to find the one which should be most associated with a set of given conditions.  Basically, it sorts with more specific records being
     * higher ranked than less specific records.  It checks expense type code; then document type level (TAA is more specific than TRV, so should be higher); then traveler type (something specific ranks higher than ALL); and finally trip type (again, something specific ranks higher than ALL)
     */
    protected class ExpenseTypeObjectCodeComparatorByHierarchyLogic implements Comparator<ExpenseTypeObjectCode> {
        @Override
        public int compare(ExpenseTypeObjectCode dora, ExpenseTypeObjectCode nora) {
            if (!StringUtils.equals(dora.getExpenseTypeCode(), nora.getExpenseTypeCode())) {
                return dora.getExpenseTypeCode().compareTo(nora.getExpenseTypeCode());
            }
            if (!StringUtils.equals(dora.getDocumentTypeName(), nora.getDocumentTypeName())) {
                final int doraDocLevel = getLevelForDocumentType(dora.getDocumentTypeName());
                final int noraDocLevel = getLevelForDocumentType(nora.getDocumentTypeName());
                final int delta = doraDocLevel - noraDocLevel;
                return delta;
            }
            if (!StringUtils.equals(dora.getTravelerTypeCode(), nora.getTravelerTypeCode())) {
                if (TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRAVELER_TYPE.equals(dora.getTravelerTypeCode())) {
                    return 1; // advantage: nora
                }
                if (TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRAVELER_TYPE.equals(nora.getTravelerTypeCode())) {
                    return -1; // advantage: dora
                }
                // we're still here?  That's...weird (we shouldn't have both EMP and NON records).  Let's have trip type figure it out for us
            }
            if (!StringUtils.equals(dora.getTripTypeCode(), nora.getTripTypeCode())) {
                if (TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRIP_TYPE.equals(dora.getTripTypeCode())) {
                    return 1; // advantage: nora
                }
                if (TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRAVELER_TYPE.equals(nora.getTripTypeCode())) {
                    return -1; // advantage: dora
                }
                if (StringUtils.isBlank(dora.getTripTypeCode())) {
                    return 1; // advantage: nora.  Though how did we get here?
                }
                return dora.getTripTypeCode().compareTo(nora.getTripTypeCode()); // another place we shouldn't ever really get to
            }
            return 0;  // odd that we got here, but whichever
        }

        /**
         * Returns the branch level of the given document type
         *
         * @param documentType the document type to find a branch level for
         * @return the branch level, with TT being high and doc types which descend from TT having lower levels
         */
        protected int getLevelForDocumentType(String documentType) {
            if (TemConstants.TravelDocTypes.TEM_TRANSACTIONAL_DOCUMENT.equals(documentType)) {
                return 3;
            } else if (TemConstants.TravelDocTypes.TRAVEL_TRANSACTIONAL_DOCUMENT.equals(documentType) || TemConstants.TravelDocTypes.TRAVEL_ENTERTAINMENT_DOCUMENT.equals(documentType) || TemConstants.TravelDocTypes.TRAVEL_RELOCATION_DOCUMENT.equals(documentType)) {
                return 2;
            } else if (TemConstants.TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT.equals(documentType) || TemConstants.TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT.equals(documentType)) {
                return 1;
            }
            return 0; // should be TAA and TAC only here
        }
    }

    @Override
    public ExpenseTypeObjectCode getExpenseTypeObjectCode(String travelExpenseCode, String documentNumber) {
        final TravelDocument document = retrieveTravelDocument(documentNumber);
        if (document != null) {
            final String documentType = document.getFinancialDocumentTypeCode();
            final String tripType = document.getTripTypeCode();
            final String travelerType = document.getTraveler().getTravelerTypeCode();
            return getExpenseType(travelExpenseCode, documentType, tripType, travelerType);
        }
        return null;
    }

    /**
     * Attempts to retrieve the TravelDocument in operation - first from the form, then by looking up via document service
     *
     * @param documentNumber the document number of the document to attempt retrieval of
     * @return the retrieved document, or null if retrieval was unsucessful
     */
    protected TravelDocument retrieveTravelDocument(String documentNumber) {
        // first, we'll look in the form
        KualiForm form = KNSGlobalVariables.getKualiForm();
        if (form instanceof TravelFormBase) {
            final TravelDocument document = ((TravelFormBase) form).getTravelDocument();
            if (!StringUtils.isBlank(document.getDocumentNumber()) && document.getDocumentNumber().equals(documentNumber)) {
                return document;
            }
        }
        // still here?  Let's look it up
        try {
            final TravelDocument document = (TravelDocument) getDocumentService().getByDocumentHeaderIdSessionless(documentNumber);
            return document;
        } catch (WorkflowException we) {
            throw new RuntimeException("Could not retrieve document " + documentNumber, we);
        }
    }

    /**
     * @see org.kuali.kfs.module.tem.service.TravelExpenseService#getExpenseTypesForDocument(java.lang.String, java.lang.String, java.lang.String, boolean)
     */
    @Override
    public List<ExpenseType> getExpenseTypesForDocument(String documentTypeName, String tripType, String travelerType, boolean groupOnly) {
        final Set<String> documentTypesForSearch = getTravelService().getParentDocumentTypeNames(documentTypeName);
        final List<ExpenseTypeObjectCode> expenseTypeObjectCodes = getExpenseTypeObjectCodeDao().findMatchingExpenseTypesObjectCodes(documentTypesForSearch, tripType, travelerType);
        Set<String> expenseTypeCodes = new HashSet<String>();
        List<ExpenseType> expenseTypes = new ArrayList<ExpenseType>();

        for (ExpenseTypeObjectCode expenseTypeObjectCode : expenseTypeObjectCodes) {
            if (!expenseTypeCodes.contains(expenseTypeObjectCode.getExpenseTypeCode())) {
                if (!groupOnly || (groupOnly && expenseTypeObjectCode.getExpenseType().isGroupTravel())) {
                    expenseTypeObjectCode.refreshReferenceObject(TemPropertyConstants.EXPENSE_TYPE);
                    expenseTypes.add(expenseTypeObjectCode.getExpenseType());
                    expenseTypeCodes.add(expenseTypeObjectCode.getExpenseTypeCode());
                }
            }
        }
        return expenseTypes;
    }

    /**
     * Comparator which compares expense types by their code
     */
    protected class ExpenseTypeComparatorByCode implements Comparator<ExpenseType> {
        @Override
        public int compare(ExpenseType lava, ExpenseType kusha) {
            if (StringUtils.equals(lava.getCode(), kusha.getCode())) {
                return 0;
            } else if (lava.getCode() == null && !StringUtils.isBlank(kusha.getCode())) {
                return 1;
            } else {
                return lava.getCode().compareTo(kusha.getCode());
            }
        }

    }

    @Override
    public HistoricalTravelExpense createHistoricalTravelExpense(AgencyStagingData agency) {

        HistoricalTravelExpense expense = new HistoricalTravelExpense();

        try {
            expense.setImportDate(dateTimeService.convertToSqlDate(agency.getProcessingTimestamp()));
        } catch (ParseException e) {
            throw new RuntimeException("Unable to convert timestamp to date " + e.getMessage());
        }

        CreditCardAgency ccAgency = agency.getCreditCardAgency();
        expense.setCreditCardAgency(ccAgency);
        expense.setCreditCardOrAgencyCode(ccAgency.getCreditCardOrAgencyCode());
        final ExpenseType expenseType = getDefaultExpenseTypeForCategory(agency.getExpenseTypeCategory());
        if (expenseType != null) {
            expense.setTravelExpenseTypeCode(expenseType.getCode());
        }
        expense.setTravelCompany(agency.getMerchantName());
        expense.setAmount(agency.getTripExpenseAmount());
        expense.setTransactionPostingDate(agency.getTransactionPostingDate());
        expense.setAgencyStagingDataId(agency.getId());
        expense.setTripId(agency.getTripId());
        expense.setProfileId(agency.getTemProfileId());

        expense.setReconciled(ReconciledCodes.UNRECONCILED);

        return expense;
    }

    /**
     * This method returns a {@link TravelExpenseTypeCode}. This is needed to convert from the code to the name for imports by traveler.
     *
     * @param expenseType
     * @return
     */
    protected TravelExpenseTypeCode getTravelExpenseTypeCode(String expenseType) {
        Map<String, String> primaryKeys = new HashMap<String, String>();
        primaryKeys.put(KFSPropertyConstants.CODE, expenseType);
        return getBusinessObjectService().findByPrimaryKey(TravelExpenseTypeCode.class, primaryKeys);
    }

    /**
     * @see org.kuali.kfs.module.tem.service.TravelExpenseService#createHistoricalTravelExpense(org.kuali.kfs.module.tem.businessobject.CreditCardStagingData)
     */
    @Override
    public HistoricalTravelExpense createHistoricalTravelExpense(CreditCardStagingData creditCard) {
        HistoricalTravelExpense expense = new HistoricalTravelExpense();

        try {
            expense.setImportDate(dateTimeService.convertToSqlDate(creditCard.getProcessingTimestamp()));
        } catch (ParseException e) {
            throw new RuntimeException("Unable to convert timestamp to date " + e.getMessage());
        }

        CreditCardAgency ccAgency = creditCard.getCreditCardAgency();
        expense.setCreditCardOrAgencyCode(ccAgency.getCreditCardOrAgencyCode());

        expense.setTravelExpenseTypeCode(creditCard.getExpenseTypeCode());
        expense.setDescription(creditCard.getExpenseTypeCode());

        expense.setTravelCompany(creditCard.getMerchantName());
        expense.setAmount(creditCard.getTransactionAmount());
        expense.setTransactionPostingDate(creditCard.getTransactionDate());
        expense.setCreditCardStagingDataId(creditCard.getId());
        expense.setProfileId(creditCard.getTemProfileId());
        expense.setLocation(creditCard.getLocation());

        expense.setReconciled(ReconciledCodes.UNRECONCILED);

        return expense;
    }

    @Override
    public HistoricalTravelExpense createHistoricalTravelExpense(AgencyStagingData agency, CreditCardStagingData creditCard, ExpenseTypeObjectCode travelExpenseType) {
        HistoricalTravelExpense expense = createHistoricalTravelExpense(agency);
        expense.setLocation(creditCard.getLocation());
        expense.setCreditCardStagingDataId(creditCard.getId());
        expense.setReconciled(ReconciledCodes.RECONCILED);
        expense.setReconciliationDate(getDateTimeService().getCurrentSqlDate());
        return expense;
    }

    @Override
    public List<AgencyStagingData> retrieveValidAgencyData() {
        Map<String, String> criteria = new HashMap<String, String>(1);
        criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, AgencyStagingDataErrorCodes.AGENCY_NO_ERROR);
        criteria.put(TemPropertyConstants.ACTIVE_IND, TemConstants.YES);
        List<AgencyStagingData> agencyData = (List<AgencyStagingData>) getBusinessObjectService().findMatching(AgencyStagingData.class, criteria);
        return agencyData;
    }

    @Override
    public List<AgencyStagingData> retrieveValidAgencyDataByImportType(String importBy) {
        Map<String, String> criteria = new HashMap<String, String>(2);
        criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, AgencyStagingDataErrorCodes.AGENCY_NO_ERROR);
        criteria.put(TemPropertyConstants.IMPORT_BY, importBy);
        List<AgencyStagingData> agencyData = (List<AgencyStagingData>) getBusinessObjectService().findMatching(AgencyStagingData.class, criteria);
        return agencyData;
    }

    @Override
    public List<CreditCardStagingData> retrieveValidCreditCardData() {
        Map<String, String> criteria = new HashMap<String, String>(1);
        criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, CreditCardStagingDataErrorCodes.CREDIT_CARD_NO_ERROR);
        criteria.put(TemPropertyConstants.IMPORT_BY, ExpenseImportTypes.IMPORT_BY_TRAVELLER);
        List<CreditCardStagingData> creditCardData = (List<CreditCardStagingData>) getBusinessObjectService().findMatching(CreditCardStagingData.class, criteria);
        return creditCardData;
    }

    @Override
    public CreditCardStagingData findImportedCreditCardExpense(KualiDecimal amount, String itineraryNumber) {
        Map<String, Object> criteria = new HashMap<String, Object>(3);
        criteria.put(TemPropertyConstants.TRANSACTION_AMOUNT, amount);
        criteria.put(TemPropertyConstants.ITINERARY_NUMBER, itineraryNumber);
        criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, CreditCardStagingDataErrorCodes.CREDIT_CARD_NO_ERROR);
        List<CreditCardStagingData> creditCardData = (List<CreditCardStagingData>) getBusinessObjectService().findMatching(CreditCardStagingData.class, criteria);
        if (ObjectUtils.isNotNull(creditCardData) && creditCardData.size() > 0) {
            return creditCardData.get(0);
        }
        return null;
    }

    @Override
    public CreditCardStagingData findImportedCreditCardExpense(KualiDecimal amount, String ticketNumber, String serviceFeeNumber) {
        Map<String, Object> criteria = new HashMap<String, Object>(4);
        criteria.put(TemPropertyConstants.TRANSACTION_AMOUNT, amount);
        criteria.put(TemPropertyConstants.TICKET_NUMBER, ticketNumber);
        criteria.put(TemPropertyConstants.SERVICE_FEE_NUMBER, serviceFeeNumber);
        criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, CreditCardStagingDataErrorCodes.CREDIT_CARD_NO_ERROR);
        List<CreditCardStagingData> creditCardData = (List<CreditCardStagingData>) getBusinessObjectService().findMatching(CreditCardStagingData.class, criteria);
        if (ObjectUtils.isNotNull(creditCardData) && creditCardData.size() > 0) {
            return creditCardData.get(0);
        }
        return null;
    }

    /**
     * This method should only be called when adding an actual expense or detail
     *
     * @see org.kuali.kfs.module.tem.service.TravelExpenseService#updateTaxabilityOfActualExpense(org.kuali.kfs.module.tem.businessobject.ActualExpense, org.kuali.kfs.module.tem.document.TravelDocument, org.kuali.rice.kim.api.identity.Person)
     */
    @Override
    public void updateTaxabilityOfActualExpense(ActualExpense actualExpense, TravelDocument document, Person currentUser) {
        if (!ObjectUtils.isNull(actualExpense.getExpenseTypeObjectCode())) {  // don't have an expense type object code? then why are we bothering?
            // 1. if the given user cannot change the taxability then we should actually look at it
            Map<String, String> permissionDetails = new HashMap<String, String>();
            permissionDetails.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, document.getDocumentTypeName());
            permissionDetails.put(KimConstants.AttributeConstants.EDIT_MODE, TemConstants.EditModes.EXPENSE_TAXABLE_MODE);
            final boolean canEditTaxable = getDocumentHelperService().getDocumentAuthorizer(document).isAuthorizedByTemplate(document, KRADConstants.KNS_NAMESPACE, KimConstants.PermissionTemplateNames.USE_TRANSACTIONAL_DOCUMENT, currentUser.getPrincipalId(), permissionDetails, Collections.<String, String>emptyMap());
            if (!canEditTaxable) {
                actualExpense.setTaxable(actualExpense.getExpenseTypeObjectCode().isTaxable());
            }
        }
    }

    /**
     * Does BusinessObjectService lookup - pretty simple
     *
     * @see org.kuali.kfs.module.tem.service.TravelExpenseService#getDefaultExpenseTypeForCategory(org.kuali.kfs.module.tem.TemConstants.ExpenseTypeMetaCategory)
     */
    @Override
    public ExpenseType getDefaultExpenseTypeForCategory(ExpenseTypeMetaCategory category) {
        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(TemPropertyConstants.EXPENSE_TYPE_META_CATEGORY_CODE, category.getCode());
        fieldValues.put(TemPropertyConstants.CATEGORY_DEFAULT, Boolean.TRUE);
        fieldValues.put(KFSPropertyConstants.ACTIVE, Boolean.TRUE);

        Collection<ExpenseType> expenseTypes = getBusinessObjectService().findMatching(ExpenseType.class, fieldValues);
        if (expenseTypes == null || expenseTypes.isEmpty()) {
            return null;
        }
        Iterator<ExpenseType> expenseTypesIterator = expenseTypes.iterator();
        ExpenseType returnValue = expenseTypesIterator.next();
        while (expenseTypesIterator.hasNext()) {
            expenseTypesIterator.next(); // exhaust result set - which already *should* be exhausted but just in case
        }
        return returnValue;
    }

    /**
     * @see org.kuali.kfs.module.tem.service.TravelExpenseService#getExpenseServiceByType(org.kuali.kfs.module.tem.TemConstants.ExpenseType)
     */
    @Override
    public TemExpenseService getExpenseServiceByType(TemConstants.ExpenseType expenseType) {
        return (TemExpenseService) SpringContext.getBean(TemExpense.class, expenseType.service);
    }

    /**
     * Gets the businessObjectService attribute.
     *
     * @return Returns the businessObjectService.
     */
    public BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    /**
     * Sets the businessObjectService attribute value.
     *
     * @param businessObjectService The businessObjectService to set.
     */
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    /**
     * Gets the dateTimeService attribute.
     *
     * @return Returns the dateTimeService.
     */
    public DateTimeService getDateTimeService() {
        return dateTimeService;
    }

    /**
     * Sets the dateTimeService attribute value.
     *
     * @param dateTimeService The dateTimeService to set.
     */
    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    private DocumentService getDocumentService() {
        return SpringContext.getBean(DocumentService.class);
    }

    public ExpenseTypeObjectCodeDao getExpenseTypeObjectCodeDao() {
        return expenseTypeObjectCodeDao;
    }

    public void setExpenseTypeObjectCodeDao(ExpenseTypeObjectCodeDao expenseTypeObjectCodeDao) {
        this.expenseTypeObjectCodeDao = expenseTypeObjectCodeDao;
    }

    public DocumentHelperService getDocumentHelperService() {
        return documentHelperService;
    }

    public void setDocumentHelperService(DocumentHelperService documentHelperService) {
        this.documentHelperService = documentHelperService;
    }

    public TravelService getTravelService() {
        return travelService;
    }

    public void setTravelService(TravelService travelService) {
        this.travelService = travelService;
    }

    /**
     * @see org.kuali.kfs.module.tem.service.TravelExpenseService#isTravelExpenseExceedReceiptRequirementThreshold(org.kuali.kfs.module.tem.businessobject.OtherExpense)
     */
    @Override
    public boolean isTravelExpenseExceedReceiptRequirementThreshold(OtherExpense expense) {
        boolean isExceed = false;

        final ExpenseTypeObjectCode expenseTypeCode = expense.getExpenseTypeObjectCode();

        if (expenseTypeCode.isReceiptRequired()) {
            //check for the threshold amount
            if (expenseTypeCode.getReceiptRequirementThreshold() != null) {
                KualiDecimal threshold = expenseTypeCode.getReceiptRequirementThreshold();
                isExceed = threshold.isLessThan(expense.getExpenseAmount());
            } else {
                isExceed = true;
            }
        }
        return isExceed;
    }

    /**
     * @see org.kuali.kfs.module.tem.service.TravelExpenseService#isTripAccountingInformationEmpty(org.kuali.kfs.module.tem.businessobject.TripAccountingInformation)
     */
    @Override
    public boolean isTripAccountingInformationEmpty(TripAccountingInformation accountingInformation) {
        return StringUtils.isEmpty(accountingInformation.getTripChartCode()) &&
            StringUtils.isEmpty(accountingInformation.getTripAccountNumber()) &&
            StringUtils.isEmpty(accountingInformation.getTripSubAccountNumber()) &&
            StringUtils.isEmpty(accountingInformation.getObjectCode()) &&
            StringUtils.isEmpty(accountingInformation.getSubObjectCode()) &&
            StringUtils.isEmpty(accountingInformation.getProjectCode()) &&
            StringUtils.isEmpty(accountingInformation.getOrganizationReference());
    }

    /**
     * Copies the parent and nulls out unnecessary fields for a detail line
     *
     * @see org.kuali.kfs.module.tem.service.TravelExpenseService#createNewDetailForActualExpense(org.kuali.kfs.module.tem.businessobject.ActualExpense)
     */
    @Override
    public ActualExpense createNewDetailForActualExpense(ActualExpense parent) {
        ActualExpense newExpense = new ActualExpense();
        try {
            BeanUtils.copyProperties(newExpense, parent);
            newExpense.setConvertedAmount(null);
            newExpense.setExpenseParentId(newExpense.getId());
            newExpense.setId(null);
            newExpense.setNotes(null);
            newExpense.setExpenseLineTypeCode(null); // evidently, this should be nulled out on detail lines
        } catch (IllegalAccessException ex) {
            LOG.error(ex.getMessage(), ex);
        } catch (InvocationTargetException ex) {
            LOG.error(ex.getMessage(), ex);
        }
        return newExpense;
    }

}
