/*
 * 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.document.service.impl;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomer;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomerAddress;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomerInvoice;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomerInvoiceDetail;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomerInvoiceRecurrenceDetails;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomerType;
import org.kuali.kfs.integration.ar.AccountsReceivableDocumentHeader;
import org.kuali.kfs.integration.ar.AccountsReceivableModuleService;
import org.kuali.kfs.integration.ar.AccountsReceivableOrganizationOptions;
import org.kuali.kfs.integration.ar.AccountsReceivableSystemInformation;
import org.kuali.kfs.kns.service.DataDictionaryService;
import org.kuali.kfs.krad.UserSession;
import org.kuali.kfs.krad.bo.Note;
import org.kuali.kfs.krad.dao.DocumentDao;
import org.kuali.kfs.krad.document.Document;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.service.KeyValuesService;
import org.kuali.kfs.krad.service.KualiRuleService;
import org.kuali.kfs.krad.service.NoteService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.KRADConstants;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.krad.workflow.service.WorkflowDocumentService;
import org.kuali.kfs.module.tem.TemConstants;
import org.kuali.kfs.module.tem.TemConstants.TravelAuthorizationParameters;
import org.kuali.kfs.module.tem.TemConstants.TravelAuthorizationStatusCodeKeys;
import org.kuali.kfs.module.tem.businessobject.AccountingDocumentRelationship;
import org.kuali.kfs.module.tem.businessobject.ExpenseTypeObjectCode;
import org.kuali.kfs.module.tem.businessobject.TemProfile;
import org.kuali.kfs.module.tem.businessobject.TravelAdvance;
import org.kuali.kfs.module.tem.businessobject.TravelerDetail;
import org.kuali.kfs.module.tem.dataaccess.TravelAuthorizationDao;
import org.kuali.kfs.module.tem.document.TravelAuthorizationAmendmentDocument;
import org.kuali.kfs.module.tem.document.TravelAuthorizationCloseDocument;
import org.kuali.kfs.module.tem.document.TravelAuthorizationDocument;
import org.kuali.kfs.module.tem.document.TravelDocument;
import org.kuali.kfs.module.tem.document.TravelReimbursementDocument;
import org.kuali.kfs.module.tem.document.service.AccountingDocumentRelationshipService;
import org.kuali.kfs.module.tem.document.service.TravelAuthorizationService;
import org.kuali.kfs.module.tem.document.service.TravelDocumentService;
import org.kuali.kfs.module.tem.service.TemProfileService;
import org.kuali.kfs.module.tem.service.TravelerService;
import org.kuali.kfs.module.tem.util.MessageUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.AccountingLine;
import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeader;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.AmountTotaling;
import org.kuali.kfs.sys.document.FinancialSystemTransactionalDocumentBase;
import org.kuali.kfs.sys.document.validation.event.AddAccountingLineEvent;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.kfs.sys.util.ObjectPopulationUtils;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.identity.IdentityService;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.springframework.transaction.annotation.Transactional;

import java.beans.PropertyChangeListener;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

import static org.kuali.kfs.module.tem.TemKeyConstants.TA_MESSAGE_CLOSE_DOCUMENT_TEXT;
import static org.kuali.kfs.module.tem.TemPropertyConstants.TRAVEL_DOCUMENT_IDENTIFIER;

@Transactional
public class TravelAuthorizationServiceImpl implements TravelAuthorizationService {

    protected static Logger LOG = Logger.getLogger(TravelAuthorizationServiceImpl.class);

    protected BusinessObjectService businessObjectService;
    protected AccountsReceivableModuleService accountsReceivableModuleService;
    protected ParameterService parameterService;
    protected DocumentService documentService;
    protected DateTimeService dateTimeService;
    protected KualiRuleService kualiRuleService;
    protected WorkflowDocumentService workflowDocumentService;
    protected UniversityDateService universityDateService;
    protected AccountingDocumentRelationshipService accountingDocumentRelationshipService;
    protected TemProfileService temProfileService;
    protected TravelDocumentService travelDocumentService;
    protected DocumentDao documentDao;
    protected DataDictionaryService dataDictionaryService;
    protected IdentityService identityService;
    protected NoteService noteService;
    protected TravelAuthorizationDao travelAuthorizationDao;

    protected List<PropertyChangeListener> propertyChangeListeners;

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelAuthorizationService#createCustomerInvoice(org.kuali.kfs.module.tem.document.TravelAuthorizationDocument)
     */
    @Override
    public void createCustomerInvoice(TravelAuthorizationDocument travelAuthorizationDocument) {

        boolean enableInvoice = parameterService.getParameterValueAsBoolean(TravelAuthorizationDocument.class, TravelAuthorizationParameters.GENERATE_INVOICE_FOR_TRAVEL_ADVANCE_IND);
        if (enableInvoice && travelAuthorizationDocument.shouldProcessAdvanceForDocument()) {
            KualiDecimal amount = travelAuthorizationDocument.getTravelAdvance().getTravelAdvanceRequested();
            if (KualiDecimal.ZERO.isLessThan(amount)) {
                TemProfile profile = travelAuthorizationDocument.getTemProfile();
                if (profile == null) {
                    //Get the TEM Profile associated with this TA
                    profile = temProfileService.findTemProfileById(travelAuthorizationDocument.getTemProfileId());
                }
                AccountsReceivableCustomer customer = profile.getCustomer();

                if (ObjectUtils.isNull(customer)) {
                    customer = createNewCustomer(profile);
                    // associate customer with traveler
                    travelAuthorizationDocument.getTraveler().setCustomerNumber(customer.getCustomerNumber());
                }

                createCustomerInvoiceFromAdvance(travelAuthorizationDocument, travelAuthorizationDocument.getTravelAdvance(), amount);
            }
        }
    }

    /**
     * Create customer invoice from advance
     *
     * @param travelAuthorizationDocument
     * @param advances
     * @param amount
     */
    protected void createCustomerInvoiceFromAdvance(final TravelAuthorizationDocument travelAuthorizationDocument, final TravelAdvance advance, final KualiDecimal amount) {

        final int numDaysDue = Integer.parseInt(parameterService.getParameterValueAsString(TravelAuthorizationDocument.class, TravelAuthorizationParameters.DUE_DATE_DAYS));
        final String invoiceItemCode = parameterService.getParameterValueAsString(TravelAuthorizationDocument.class, TravelAuthorizationParameters.TRAVEL_ADVANCE_INVOICE_ITEM_CODE);
        final String processingOrgCode = parameterService.getParameterValueAsString(TravelAuthorizationDocument.class, TravelAuthorizationParameters.TRAVEL_ADVANCE_BILLING_ORGANIZATION);
        final String processingChartCode = parameterService.getParameterValueAsString(TravelAuthorizationDocument.class, TravelAuthorizationParameters.TRAVEL_ADVANCE_BILLING_CHART);

        try {
            GlobalVariables.doInNewGlobalVariables(new UserSession(KFSConstants.SYSTEM_USER), new Callable<Object>() {

                @Override
                public Object call() {
                    // need to refactor this so the customer id is stored on the doc, not in travel advances
                    Calendar cal = Calendar.getInstance();
                    String customerNumber = travelAuthorizationDocument.getTemProfile().getCustomerNumber();
                    String orgInvoiceNumber = travelAuthorizationDocument.getTravelDocumentIdentifier();
                    java.util.Date billingDate = dateTimeService.getCurrentDate();
                    cal.setTime(travelAuthorizationDocument.getTripEnd());
                    cal.add(Calendar.DATE, numDaysDue);
                    java.util.Date dueDate = cal.getTime();

                    AccountsReceivableCustomerInvoice customerInvoiceDocument = accountsReceivableModuleService.createCustomerInvoiceDocument();
                    LOG.info("Created customer invoice document " + customerInvoiceDocument.getDocumentNumber());

                    setupDefaultValuesForNewCustomerInvoiceDocument(customerInvoiceDocument, processingChartCode, processingOrgCode);

                    customerInvoiceDocument.getDocumentHeader().setOrganizationDocumentNumber(travelAuthorizationDocument.getTravelDocumentIdentifier());
                    customerInvoiceDocument.getDocumentHeader().setDocumentDescription("Travel Advance - " + travelAuthorizationDocument.getTravelDocumentIdentifier() + " - " + travelAuthorizationDocument.getTraveler().getFirstName() + " " + travelAuthorizationDocument.getTraveler().getLastName());

                    final int documentDescriptionMaxLength = getDataDictionaryService().getAttributeMaxLength(customerInvoiceDocument.getDocumentHeader().getClass(), KFSPropertyConstants.DOCUMENT_DESCRIPTION);
                    if (customerInvoiceDocument.getDocumentHeader().getDocumentDescription().length() >= documentDescriptionMaxLength) {
                        String truncatedDocumentDescription = customerInvoiceDocument.getDocumentHeader().getDocumentDescription().substring(0, documentDescriptionMaxLength - 1);
                        customerInvoiceDocument.getDocumentHeader().setDocumentDescription(truncatedDocumentDescription);
                    }
                    customerInvoiceDocument.getAccountsReceivableDocumentHeader().setCustomerNumber(customerNumber);
                    customerInvoiceDocument.setBillingDate(new java.sql.Date(billingDate.getTime()));
                    customerInvoiceDocument.setInvoiceDueDate(new java.sql.Date(dueDate.getTime()));
                    customerInvoiceDocument.setOrganizationInvoiceNumber(orgInvoiceNumber.toString());

                    //Make sure the address from the TA is a customer address for the Invoice that is getting created
                    AccountsReceivableCustomerAddress customerBillToAddress = null;
                    TravelerDetail traveler = travelAuthorizationDocument.getTraveler();
                    TemProfile profile = travelAuthorizationDocument.getTemProfile();
                    if (profile == null) {
                        //Get the TEM Profile associated with this TA
                        profile = temProfileService.findTemProfileById(travelAuthorizationDocument.getTemProfileId());
                    }
                    AccountsReceivableCustomer customer = profile.getCustomer();

                    //Compare the address from the TA to the addresses for this customer to see if it already exists
                    for (AccountsReceivableCustomerAddress address : customer.getAccountsReceivableCustomerAddresses()) {
                        if (!compareAddress(address, traveler)) {
                            //Address found
                            customerBillToAddress = address;
                            break;
                        }
                    }

                    if (customerBillToAddress == null) {
                        //This address from the TA was not found as a customer address so create a new one for this customer
                        customerBillToAddress = accountsReceivableModuleService.createCustomerAddress();
                        customerBillToAddress.setCustomerAddressTypeCodeAsAlternate();

                        //Customer's name as the customer address name
                        String tempName = profile.getFirstName() + " " + (StringUtils.isEmpty(profile.getMiddleName()) ? "" : profile.getMiddleName() + " ") + profile.getLastName();
                        if (tempName.length() > 40) {
                            tempName = profile.getFirstName() + " " + profile.getLastName();
                            while (tempName.length() > 40) {
                                tempName = tempName.substring(0, tempName.length() - 1);
                            }
                        }

                        //Set all the fields for the new address
                        customerBillToAddress.setCustomerAddressName(tempName);
                        customer.setCustomerAddressChangeDate(dateTimeService.getCurrentSqlDate());
                        customerBillToAddress.setCustomerLine1StreetAddress(StringUtils.isNotEmpty(traveler.getStreetAddressLine1()) ? traveler.getStreetAddressLine1().toUpperCase() : "");
                        customerBillToAddress.setCustomerLine2StreetAddress(StringUtils.isNotEmpty(traveler.getStreetAddressLine2()) ? traveler.getStreetAddressLine2().toUpperCase() : "");
                        customerBillToAddress.setCustomerCityName(StringUtils.isNotEmpty(traveler.getCityName()) ? traveler.getCityName().toUpperCase() : "");
                        customerBillToAddress.setCustomerStateCode(StringUtils.isNotEmpty(traveler.getStateCode()) ? traveler.getStateCode().toUpperCase() : "");
                        customerBillToAddress.setCustomerZipCode(traveler.getZipCode());
                        customerBillToAddress.setCustomerCountryCode(StringUtils.isNotEmpty(traveler.getCountryCode()) ? traveler.getCountryCode().toUpperCase() : "");
                        customerBillToAddress.setCustomerEmailAddress(StringUtils.isNotEmpty(traveler.getEmailAddress()) ? traveler.getEmailAddress().toUpperCase() : "");

                        //Add the new address to the customer and save
                        List<AccountsReceivableCustomerAddress> customerAddresses = customer.getAccountsReceivableCustomerAddresses();
                        customerAddresses.add(customerBillToAddress);
                        customer.setAccountsReceivableCustomerAddresses(customerAddresses);
                        accountsReceivableModuleService.saveCustomer(customer);
                    }

                    customerBillToAddress.refresh();

                    customerInvoiceDocument.setCustomerBillToAddress(customerBillToAddress);
                    customerInvoiceDocument.setCustomerBillToAddressIdentifier(customerBillToAddress.getCustomerAddressIdentifier());
                    customerInvoiceDocument.setBillingAddressTypeCodeAsPrimary();
                    customerInvoiceDocument.setBillingAddressName(customer.getCustomerName());
                    customerInvoiceDocument.setBillingLine1StreetAddress(customerBillToAddress.getCustomerLine1StreetAddress());
                    customerInvoiceDocument.setBillingLine2StreetAddress(customerBillToAddress.getCustomerLine2StreetAddress());
                    customerInvoiceDocument.setBillingCityName(customerBillToAddress.getCustomerCityName());
                    customerInvoiceDocument.setBillingStateCode(customerBillToAddress.getCustomerStateCode());
                    customerInvoiceDocument.setBillingZipCode(customerBillToAddress.getCustomerZipCode());
                    customerInvoiceDocument.setBillingCountryCode(customerBillToAddress.getCustomerCountryCode());
                    customerInvoiceDocument.setBillingAddressInternationalProvinceName(customerBillToAddress.getCustomerAddressInternationalProvinceName());
                    customerInvoiceDocument.setBillingInternationalMailCode(customerBillToAddress.getCustomerInternationalMailCode());
                    customerInvoiceDocument.setBillingEmailAddress(customerBillToAddress.getCustomerEmailAddress());

                    try {
                        LOG.info("Saving customer invoice document " + customerInvoiceDocument.getDocumentNumber());
                        // getDocumentService().saveDocument(customerInvoiceDocument);
                        if (StringUtils.isEmpty(advance.getArInvoiceDocNumber())) {
                            AccountsReceivableCustomerInvoiceDetail detail = createInvoiceDetailFromAdvance(advance, customerInvoiceDocument.getDocumentNumber(), invoiceItemCode, processingOrgCode, processingChartCode);
                            addInvoiceDetailToDocument(detail, customerInvoiceDocument);
                        }

                        LOG.info("Saving customer invoice document after adding acctg lines " + customerInvoiceDocument.getDocumentNumber());
                        accountsReceivableModuleService.saveCustomerInvoiceDocument(customerInvoiceDocument);

                        // add relationship
                        String relationDescription = "TA - Customer Invoice";
                        accountingDocumentRelationshipService.save(new AccountingDocumentRelationship(travelAuthorizationDocument.getDocumentNumber(), customerInvoiceDocument.getDocumentNumber(), relationDescription));

                        //update AR Invoice Doc number to the travel advances
                        if (StringUtils.isEmpty(advance.getArInvoiceDocNumber())) {
                            advance.setArInvoiceDocNumber(customerInvoiceDocument.getDocumentNumber());
                            advance.setArCustomerId(customerNumber);
                        }

                        // route
                        WorkflowDocument originalWorkflowDocument = customerInvoiceDocument.getDocumentHeader().getWorkflowDocument();
                        try {
                            WorkflowDocument newWorkflowDocument = workflowDocumentService.loadWorkflowDocument(customerInvoiceDocument.getDocumentNumber(), GlobalVariables.getUserSession().getPerson());
                            newWorkflowDocument.setTitle(originalWorkflowDocument.getTitle());

                            customerInvoiceDocument.getDocumentHeader().setWorkflowDocument(newWorkflowDocument);

                            accountsReceivableModuleService.blanketApproveCustomerInvoiceDocument(customerInvoiceDocument);
                        } finally {
                            customerInvoiceDocument.getDocumentHeader().setWorkflowDocument(originalWorkflowDocument);
                        }
                        LOG.info("Submitted customer invoice document " + customerInvoiceDocument.getDocumentNumber() + " for " + customerNumber + " - " + dateTimeService.toDateString(billingDate) + "\n\n");

                    } catch (WorkflowException e) {
                        throw new RuntimeException("Customer Invoice Document routing failed.");
                    }
                    return null;
                }

            });
        } catch (Exception e) {
            LOG.error(e.toString());
        }

    }

    /**
     * @param customerAddress
     * @param traveler
     * @return
     */
    protected boolean compareAddress(AccountsReceivableCustomerAddress customerAddress, TravelerDetail traveler) {
        if (!StringUtils.equalsIgnoreCase(customerAddress.getCustomerLine1StreetAddress(), traveler.getStreetAddressLine1())) {
            return true;
        }
        if (!StringUtils.equalsIgnoreCase(customerAddress.getCustomerLine2StreetAddress(), traveler.getStreetAddressLine2())) {
            return true;
        }
        if (!StringUtils.equalsIgnoreCase(customerAddress.getCustomerCityName(), traveler.getCityName())) {
            return true;
        }
        if (!StringUtils.equalsIgnoreCase(customerAddress.getCustomerStateCode(), traveler.getStateCode())) {
            return true;
        }
        if (!StringUtils.equalsIgnoreCase(customerAddress.getCustomerZipCode(), traveler.getZipCode())) {
            return true;
        }
        if (!StringUtils.equalsIgnoreCase(customerAddress.getCustomerCountryCode(), traveler.getCountryCode())) {
            return true;
        }

        return false;
    }

    /**
     * @param document
     * @param chartOfAccountsCode
     * @param organizationCode
     */
    protected void setupDefaultValuesForNewCustomerInvoiceDocument(AccountsReceivableCustomerInvoice document, String chartOfAccountsCode, String organizationCode) {

        // setupBasicDefaultValuesForCustomerInvoiceDocument(document);
        document.setBillByChartOfAccountCode(chartOfAccountsCode);
        document.setBilledByOrganizationCode(organizationCode);
        document.setOpenInvoiceIndicator(true);

        // set up the default values for the AR DOC Header

        AccountsReceivableDocumentHeader accountsReceivableDocumentHeader = accountsReceivableModuleService.createAccountsReceivableDocumentHeader();

        // we try to get the processing org directly, which we'll get if the initiating user is an AR Processor
        // Remove once we switch our parameters
        AccountsReceivableSystemInformation processingOrg = accountsReceivableModuleService.getSystemInformationByProcessingChartOrgAndFiscalYear(chartOfAccountsCode, organizationCode, universityDateService.getCurrentFiscalYear());
        if (processingOrg != null) {
            accountsReceivableDocumentHeader.setProcessingChartOfAccountCode(processingOrg.getProcessingChartOfAccountCode());
            accountsReceivableDocumentHeader.setProcessingOrganizationCode(processingOrg.getProcessingOrganizationCode());
            // return accountsReceivableDocumentHeader;
        }

        // next we try to get the processing org through the initiating user's billing org, if that exists
        AccountsReceivableOrganizationOptions orgOptions = accountsReceivableModuleService.getOrgOptionsIfExists(chartOfAccountsCode, organizationCode);
        if (orgOptions != null) {
            accountsReceivableDocumentHeader.setProcessingChartOfAccountCode(orgOptions.getProcessingChartOfAccountCode());
            accountsReceivableDocumentHeader.setProcessingOrganizationCode(orgOptions.getProcessingOrganizationCode());
            // return accountsReceivableDocumentHeader;
        }

        accountsReceivableDocumentHeader.setDocumentNumber(document.getDocumentNumber());
        document.setAccountsReceivableDocumentHeader(accountsReceivableDocumentHeader);

        // set up the primary key for AR_INV_RCURRNC_DTL_T
        AccountsReceivableCustomerInvoiceRecurrenceDetails recurrenceDetails = accountsReceivableModuleService.createCustomerInvoiceRecurrenceDetails();
        recurrenceDetails.setInvoiceNumber(document.getDocumentNumber());
        // recurrenceDetails.setCustomerNumber(document.getCustomer().getCustomerNumber());
        document.setCustomerInvoiceRecurrenceDetails(recurrenceDetails);

        AccountsReceivableOrganizationOptions organizationOptions = accountsReceivableModuleService.getOrgOptionsIfExists(document.getBillByChartOfAccountCode(), document.getBilledByOrganizationCode());

        if (ObjectUtils.isNotNull(organizationOptions)) {
            document.setPrintInvoiceIndicator(organizationOptions.getPrintInvoiceIndicator());
            document.setInvoiceTermsText(organizationOptions.getOrganizationPaymentTermsText());
        }
    }

    protected AccountsReceivableCustomerInvoiceDetail createInvoiceDetailFromAdvance(TravelAdvance advance, String documentNumber, String invoiceItemCode, String processingOrgCode, String processingChartCode) {
        AccountsReceivableCustomerInvoiceDetail customerInvoiceDetail = accountsReceivableModuleService.getCustomerInvoiceDetailFromCustomerInvoiceItemCode(invoiceItemCode, processingChartCode, processingOrgCode);

        customerInvoiceDetail.setDocumentNumber(documentNumber);
        customerInvoiceDetail.setInvoiceItemUnitPrice(advance.getTravelAdvanceRequested());
        customerInvoiceDetail.setInvoiceItemQuantity(new BigDecimal(1));

        customerInvoiceDetail.updateAmountBasedOnQuantityAndUnitPrice();
        String accountsReceivableObjectCode = accountsReceivableModuleService.getAccountsReceivableObjectCodeBasedOnReceivableParameter(customerInvoiceDetail);
        customerInvoiceDetail.setAccountsReceivableObjectCode(accountsReceivableObjectCode);
        return customerInvoiceDetail;
    }


    /**
     * This method takes a detail object, runs business rules and adds it to the source accounting lines of the invoice
     *
     * @param detail
     * @param customerInvoiceDocument
     */
    protected void addInvoiceDetailToDocument(AccountsReceivableCustomerInvoiceDetail detail, AccountsReceivableCustomerInvoice customerInvoiceDocument) {
        accountsReceivableModuleService.recalculateCustomerInvoiceDetail(customerInvoiceDocument, detail);

        // run rules
        boolean rulePassed = true;
        // check any business rules
        rulePassed &= kualiRuleService.applyRules(new AddAccountingLineEvent(KFSConstants.NEW_SOURCE_ACCT_LINE_PROPERTY_NAME, (Document) customerInvoiceDocument, (AccountingLine) detail));

        LOG.debug("running rules on new source line : " + rulePassed);
        // add accountingLine
        detail.refreshNonUpdateableReferences();
        accountsReceivableModuleService.prepareCustomerInvoiceDetailForAdd(detail, customerInvoiceDocument);
        customerInvoiceDocument.addSourceAccountingLine((SourceAccountingLine) detail);
        if (customerInvoiceDocument instanceof AmountTotaling) {
            ((FinancialSystemDocumentHeader) customerInvoiceDocument.getDocumentHeader()).setFinancialDocumentTotalAmount(((AmountTotaling) customerInvoiceDocument).getTotalDollarAmount());
        }
    }

    /**
     * Locate all {@link TravelAuthorizationDocument} instances with the same <code>travelDocumentIdentifier</code>
     *
     * @param travelDocumentIdentifier to locate {@link TravelAuthorizationDocument} instances
     * @return {@link Collection} of {@link TravelAuthorizationDocument} instances
     */
    @Override
    public Collection<TravelAuthorizationDocument> find(final String travelDocumentIdentifier) {
        final Map<String, Object> criteria = new HashMap<String, Object>();
        criteria.put(TRAVEL_DOCUMENT_IDENTIFIER, travelDocumentIdentifier);
        return businessObjectService.findMatching(TravelAuthorizationDocument.class, criteria);
    }

    public void addListenersTo(final TravelAuthorizationDocument authorization) {
        authorization.setPropertyChangeListeners(propertyChangeListeners);
    }

    /**
     * Locate all {@link TravelAuthorizationAmendmentDocument} instances with the same <code>travelDocumentIdentifier</code>
     *
     * @param travelDocumentIdentifier to locate {@link TravelAuthorizationAmendmentDocument} instances
     * @return {@link Collection} of {@link TravelAuthorizationAmendmentDocument} instances
     */
    @Override
    public Collection<TravelAuthorizationAmendmentDocument> findAmendment(Integer travelDocumentIdentifier) {
        final Map<String, Object> criteria = new HashMap<String, Object>();
        criteria.put(TRAVEL_DOCUMENT_IDENTIFIER, travelDocumentIdentifier);
        return businessObjectService.findMatching(TravelAuthorizationAmendmentDocument.class, criteria);
    }

    /**
     * This method creates a new travel auth document from a source document
     *
     * @param sourceDocument
     * @param docType
     * @return new Travel Authorization Document
     * @throws WorkflowException
     */
    @SuppressWarnings("rawtypes")
    protected TravelAuthorizationDocument createTravelAuthorizationDocumentFromSourceDocument(TravelDocument sourceDocument, String docType) throws WorkflowException {
        if (ObjectUtils.isNull(sourceDocument)) {
            String errorMsg = "Attempting to create new Travel Authorization of type '" + docType + "' from source TA doc that is null";
            LOG.error(errorMsg);
            throw new RuntimeException(errorMsg);
        }

        TravelAuthorizationDocument newTravelAuthChangeDocument = (TravelAuthorizationDocument) documentService.getNewDocument(docType);

        Set<Class> classesToExclude = new HashSet<Class>();
        Class sourceObjectClass = FinancialSystemTransactionalDocumentBase.class;
        classesToExclude.add(sourceObjectClass);
        while (sourceObjectClass.getSuperclass() != null) {
            sourceObjectClass = sourceObjectClass.getSuperclass();
            classesToExclude.add(sourceObjectClass);
        }
        ObjectPopulationUtils.populateFromBaseWithSuper(sourceDocument, newTravelAuthChangeDocument, TemConstants.uncopyableFieldsForTravelAuthorization(), classesToExclude);
        newTravelAuthChangeDocument.getDocumentHeader().setDocumentDescription(sourceDocument.getDocumentHeader().getDocumentDescription());
        newTravelAuthChangeDocument.getDocumentHeader().setOrganizationDocumentNumber(sourceDocument.getDocumentHeader().getOrganizationDocumentNumber());
        newTravelAuthChangeDocument.getDocumentHeader().setExplanation(sourceDocument.getDocumentHeader().getExplanation());

        newTravelAuthChangeDocument.refreshNonUpdateableReferences();

        return newTravelAuthChangeDocument;
    }


    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelAuthorizationService#getTravelAuthorizationBy(java.lang.String)
     */
    @Override
    public TravelAuthorizationDocument getTravelAuthorizationBy(String documentNumber) {
        if (ObjectUtils.isNotNull(documentNumber)) {
            try {
                TravelAuthorizationDocument doc = (TravelAuthorizationDocument) documentService.getByDocumentHeaderId(documentNumber);
                if (ObjectUtils.isNotNull(doc)) {
                    WorkflowDocument workflowDocument = doc.getDocumentHeader().getWorkflowDocument();
                    doc.refreshReferenceObject(KFSPropertyConstants.DOCUMENT_HEADER);
                    doc.getDocumentHeader().setWorkflowDocument(workflowDocument);
                }
                return doc;
            } catch (WorkflowException e) {
                String errorMessage = "Error getting travel authorization document from document service";
                LOG.error("getTravelAuthorizationByDocumentNumber() " + errorMessage, e);
                throw new RuntimeException(errorMessage, e);
            }
        }
        return null;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelAuthorizationService#closeAuthorization(org.kuali.kfs.module.tem.document.TravelAuthorizationDocument, java.lang.String, java.lang.String, java.lang.String)
     */
    @Override
    public TravelAuthorizationCloseDocument closeAuthorization(TravelAuthorizationDocument authorization, String annotation, String initiatorPrincipalName, String reimbursementDocNum) {
        TravelAuthorizationCloseDocument authorizationClose = null;
        try {
            String user = GlobalVariables.getUserSession().getPerson().getLastName() + ", " + GlobalVariables.getUserSession().getPerson().getFirstName();
            String note = MessageUtils.getMessage(TA_MESSAGE_CLOSE_DOCUMENT_TEXT, user);
            Principal kfsSystemUser = getIdentityService().getPrincipalByPrincipalName(KFSConstants.SYSTEM_USER);

            authorization.updateAndSaveAppDocStatus(TravelAuthorizationStatusCodeKeys.RETIRED_VERSION);
            documentDao.save(authorization);

            // setting to initiator principal name
            GlobalVariables.setUserSession(new UserSession(initiatorPrincipalName));
            authorizationClose = authorization.toCopyTAC();
            final Note newNoteTAC = documentService.createNoteFromDocument(authorizationClose, note);
            newNoteTAC.setAuthorUniversalIdentifier(kfsSystemUser.getPrincipalId());
            authorizationClose.addNote(newNoteTAC);
            authorizationClose.setTravelReimbursementDocumentNumber(reimbursementDocNum);

            // add relationship
            String relationDescription = authorization.getDocumentTypeName() + " - " + authorizationClose.getDocumentTypeName();
            accountingDocumentRelationshipService.save(new AccountingDocumentRelationship(authorization.getDocumentNumber(), authorizationClose.getDocumentNumber(), relationDescription));

            // switching to KR user to route
            GlobalVariables.setUserSession(new UserSession(KRADConstants.SYSTEM_USER));
            authorizationClose.setApplicationDocumentStatus(TravelAuthorizationStatusCodeKeys.CLOSED);
            documentService.routeDocument(authorizationClose, annotation, null);
        } catch (Exception e) {
            LOG.error("Could not create TAC or route it with travel id " + authorization.getTravelDocumentIdentifier());
            LOG.error(e.getMessage(), e);
        }
        return authorizationClose;
    }

    protected AccountsReceivableCustomer createNewCustomer(TemProfile profile) {
        profile.setCustomer(accountsReceivableModuleService.createCustomer());
        profile.getCustomer().setCustomerName(profile.getName());

        String newCustNumber = accountsReceivableModuleService.getNextCustomerNumber(profile.getCustomer());
        newCustNumber = newCustNumber.toUpperCase();
        profile.setCustomerNumber(newCustNumber);
        profile.getCustomer().setCustomerNumber(newCustNumber);
        businessObjectService.save(profile);

        //Set to customer type code to travel and make the customer active
        String customerTypeCode = "";
        List<AccountsReceivableCustomerType> customerTypes = accountsReceivableModuleService.findByCustomerTypeDescription(TemConstants.CUSTOMER_TRAVLER_TYPE_CODE);
        for (AccountsReceivableCustomerType customerType : customerTypes) {
            customerTypeCode = customerType.getCustomerTypeCode();
            break;
        }
        profile.getCustomer().setCustomerTypeCode(customerTypeCode);
        profile.getCustomer().setActive(true);

        SpringContext.getBean(TravelerService.class).copyTemProfileToCustomer(profile, profile.getCustomer());
        accountsReceivableModuleService.saveCustomer(profile.getCustomer());

        return profile.getCustomer();
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelAuthorizationService#findEnrouteOrApprovedTravelReimbursement(org.kuali.kfs.module.tem.document.TravelAuthorizationDocument)
     */
    @Override
    public TravelReimbursementDocument findEnrouteOrProcessedTravelReimbursement(TravelAuthorizationDocument authorization) {

        TravelReimbursementDocument reimbursement = null;
        List<TravelReimbursementDocument> reimbursementDocumentList = travelDocumentService.findReimbursementDocuments(authorization.getTravelDocumentIdentifier());

        //look for enroute TR document - return the first document if any is found
        for (TravelReimbursementDocument document : reimbursementDocumentList) {
            WorkflowDocument workflowDocument = document.getDocumentHeader().getWorkflowDocument();
            if (workflowDocument.isEnroute() || workflowDocument.isFinal() || workflowDocument.isProcessed()) {
                reimbursement = document;
            }
        }
        return reimbursement;
    }

    /**
     * This method checks to see if the travel expense type code is a prepaid expense
     *
     * @param travelExpenseTypeCodeCode
     */
    public boolean checkNonReimbursable(String travelExpenseTypeCodeId) {
        boolean nonReimbursable = false;
        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put("travelExpenseTypeCodeId", travelExpenseTypeCodeId);
        Collection<ExpenseTypeObjectCode> keyValueList = SpringContext.getBean(KeyValuesService.class).findMatching(ExpenseTypeObjectCode.class, fieldValues);
        //should only return 1
        for (ExpenseTypeObjectCode typeCode : keyValueList) {
            nonReimbursable = typeCode.getExpenseType().isPrepaidExpense();
        }
        return nonReimbursable;
    }

    @Override
    public List<String> findMatchingTrips(TravelAuthorizationDocument authorization) {
        List<String> duplicateTrips = new ArrayList<String>();

        Date tripBeginBufferDate = authorization.getTripBegin() == null ? null : getTripBeginDate(authorization.getTripBegin());
        Date tripEndBufferDate = authorization.getTripEnd() == null ? null : getTripEndDate(authorization.getTripEnd());
        if (tripBeginBufferDate == null || tripEndBufferDate == null) {
            return duplicateTrips; // return the empty list.  don't worry though - without a trip begin or end, they're certainly coming back
        }

        List<TravelAuthorizationDocument> authorizationDocuments = travelAuthorizationDao.findTravelAuthorizationByTraveler(authorization.getTemProfileId());

        for (TravelAuthorizationDocument authorizationDocument : authorizationDocuments) {

            List<TravelReimbursementDocument> travelReimbursementDocuments = travelDocumentService.findReimbursementDocuments(authorizationDocument.getTravelDocumentIdentifier());

            if (!ObjectUtils.isNull(travelReimbursementDocuments) && !travelReimbursementDocuments.isEmpty()) {
                boolean matchFound = matchReimbursements(travelReimbursementDocuments, tripBeginBufferDate, tripEndBufferDate);
                if (matchFound) {
                    duplicateTrips.add(authorizationDocument.getDocumentNumber());
                }
            } else {
                // look for TA's
                Date tripBeginDate = convertToSqlDate(authorizationDocument.getTripBegin());
                Date tripEndDate = convertToSqlDate(authorizationDocument.getTripEnd());
                if (!authorization.getDocumentNumber().equals(authorizationDocument.getDocumentNumber()) && doesDatesOverlap(tripBeginBufferDate, tripEndBufferDate, tripBeginDate, tripEndDate)) {

                    duplicateTrips.add(authorizationDocument.getDocumentNumber());
                }
            }
        }


        return duplicateTrips;

    }

    private Date convertToSqlDate(Timestamp date) {
        Date convertedDate = null;
        try {
            convertedDate = dateTimeService.convertToSqlDate(date);

        } catch (ParseException ex) {
            LOG.error("Parse exception " + ex);
        }

        return convertedDate;
    }

    public boolean doesDatesOverlap(Date tripBeginBufferDate, Date tripEndBufferDate, Date tripBeginDate, Date tripEndDate) {
        if ((tripBeginDate.compareTo(tripBeginBufferDate) >= 0 && tripBeginDate.compareTo(tripEndBufferDate) <= 0) ||
            (tripEndDate.compareTo(tripBeginBufferDate) >= 0 && tripEndDate.compareTo(tripEndBufferDate) <= 0)) {
            return true;
        }
        return false;
    }

    private boolean matchReimbursements(List<TravelReimbursementDocument> travelReimbursementDocuments, Date tripBeginBufferDate, Date tripEndBufferDate) {
        Timestamp earliestTripBeginDate = null;
        Timestamp greatestTripEndDate = null;

        for (TravelReimbursementDocument document : travelReimbursementDocuments) {
            Timestamp tripBegin = document.getTripBegin();
            Timestamp tripEnd = document.getTripEnd();
            if (ObjectUtils.isNull(earliestTripBeginDate) && ObjectUtils.isNull(greatestTripEndDate)) {
                earliestTripBeginDate = tripBegin;
                greatestTripEndDate = tripEnd;
            } else {
                earliestTripBeginDate = tripBegin.before(earliestTripBeginDate) ? tripBegin : earliestTripBeginDate;
                greatestTripEndDate = tripEnd.after(greatestTripEndDate) ? tripEnd : greatestTripEndDate;

            }
        }

        if (doesDatesOverlap(tripBeginBufferDate, tripEndBufferDate, convertToSqlDate(earliestTripBeginDate), convertToSqlDate(greatestTripEndDate))) {
            return true;
        }


        return false;
    }

    private Integer getDuplicateTripDateRangeDays() {
        String tripDateRangeDays = parameterService.getParameterValueAsString(TravelAuthorizationDocument.class, TemConstants.TravelParameters.DUPLICATE_TRIP_DATE_RANGE_DAYS);
        Integer days = null;
        if (!StringUtils.isNumeric(tripDateRangeDays)) {
            days = TemConstants.DEFAULT_DUPLICATE_TRIP_DATE_RANGE_DAYS;
        }

        days = Integer.parseInt(tripDateRangeDays);
        return days;

    }

    private Date getTripBeginDate(Timestamp tripBeginDate) {
        Date tripBegin = null;
        Integer days = getDuplicateTripDateRangeDays();
        try {
            tripBegin = dateTimeService.convertToSqlDate(dateTimeService.toDateString(DateUtils.addDays(tripBeginDate, (days * -1))));

        } catch (ParseException pe) {
            LOG.error("Exception while parsing trip begin date" + pe);
        }

        return tripBegin;

    }

    private Date getTripEndDate(Timestamp tripEndDate) {
        Date tripEnd = null;
        Integer days = getDuplicateTripDateRangeDays();
        try {
            tripEnd = dateTimeService.convertToSqlDate(dateTimeService.toDateString((DateUtils.addDays(tripEndDate, days))));

        } catch (ParseException pe) {
            LOG.error("Exception while parsing trip end date" + pe);
        }

        return tripEnd;

    }


    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    public void setAccountsReceivableModuleService(AccountsReceivableModuleService accountsReceivableModuleService) {
        this.accountsReceivableModuleService = accountsReceivableModuleService;
    }

    public void setPropertyChangeListeners(final List<PropertyChangeListener> propertyChangeListeners) {
        this.propertyChangeListeners = propertyChangeListeners;
    }

    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    public void setDocumentService(DocumentService documentService) {
        this.documentService = documentService;
    }

    public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
        this.workflowDocumentService = workflowDocumentService;
    }

    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    public void setTravelDocumentService(TravelDocumentService travelDocumentService) {
        this.travelDocumentService = travelDocumentService;
    }

    public void setUniversityDateService(UniversityDateService universityDateService) {
        this.universityDateService = universityDateService;
    }

    public void setRuleService(final KualiRuleService kualiRuleService) {
        this.kualiRuleService = kualiRuleService;
    }

    public void setAccountingDocumentRelationshipService(AccountingDocumentRelationshipService accountingDocumentRelationshipService) {
        this.accountingDocumentRelationshipService = accountingDocumentRelationshipService;
    }

    public void setTemProfileService(TemProfileService temProfileService) {
        this.temProfileService = temProfileService;
    }

    public void setDocumentDao(DocumentDao documentDao) {
        this.documentDao = documentDao;
    }

    /**
     * @return the injected implementation of the data dictionary service
     */
    public DataDictionaryService getDataDictionaryService() {
        return dataDictionaryService;
    }

    /**
     * Injects an implementation of the DataDictionaryService
     *
     * @param dataDictionaryService an implementation of the DataDictionaryService to inject
     */
    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
        this.dataDictionaryService = dataDictionaryService;
    }

    public IdentityService getIdentityService() {
        return identityService;
    }

    public void setIdentityService(IdentityService identityService) {
        this.identityService = identityService;
    }

    public NoteService getNoteService() {
        return noteService;
    }

    public void setNoteService(NoteService noteService) {
        this.noteService = noteService;
    }

    public void setTravelAuthorizationDao(TravelAuthorizationDao travelAuthorizationDao) {
        this.travelAuthorizationDao = travelAuthorizationDao;
    }


}
