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


import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.ErrorMessage;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.tem.TemConstants;
import org.kuali.kfs.module.tem.TemConstants.AgencyMatchProcessParameter;
import org.kuali.kfs.module.tem.TemConstants.AgencyStagingDataErrorCodes;
import org.kuali.kfs.module.tem.TemConstants.AgencyStagingDataValidation;
import org.kuali.kfs.module.tem.TemConstants.CreditCardStagingDataErrorCodes;
import org.kuali.kfs.module.tem.TemConstants.TravelParameters;
import org.kuali.kfs.module.tem.TemKeyConstants;
import org.kuali.kfs.module.tem.TemParameterConstants;
import org.kuali.kfs.module.tem.TemPropertyConstants;
import org.kuali.kfs.module.tem.batch.AgencyDataImportStep;
import org.kuali.kfs.module.tem.batch.service.ExpenseImportByTripService;
import org.kuali.kfs.module.tem.batch.service.ImportedExpensePendingEntryService;
import org.kuali.kfs.module.tem.businessobject.AgencyServiceFee;
import org.kuali.kfs.module.tem.businessobject.AgencyStagingData;
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.TemSourceAccountingLine;
import org.kuali.kfs.module.tem.businessobject.TripAccountingInformation;
import org.kuali.kfs.module.tem.document.TravelDocument;
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.TravelEncumbranceService;
import org.kuali.kfs.module.tem.service.TravelExpenseService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Service for handling imported expenses using the IU method or "By Trip Id" method.
 *
 * @see org.kuali.kfs.module.tem.document.validation.impl.AgencyStagingDataRule
 */
@Transactional
public class ExpenseImportByTripServiceImpl extends ExpenseImportServiceBase implements ExpenseImportByTripService {

    public static Logger LOG = Logger.getLogger(ExpenseImportByTripServiceImpl.class);

    private TravelAuthorizationService travelAuthorizationService;
    private TemProfileService temProfileService;
    private BusinessObjectService businessObjectService;
    private DateTimeService dateTimeService;
    private TravelExpenseService travelExpenseService;
    private ImportedExpensePendingEntryService importedExpensePendingEntryService;
    private TravelDocumentService travelDocumentService;
    private TravelEncumbranceService travelEncumbranceService;

    /**
     * @see org.kuali.kfs.module.tem.batch.service.ExpenseImportByTripService#areMandatoryFieldsPresent(org.kuali.kfs.module.tem.businessobject.AgencyStagingData)
     */
    @Override
    public List<ErrorMessage> validateMandatoryFieldsPresent(AgencyStagingData agencyData) {

        List<ErrorMessage> errorMessages = new ArrayList<ErrorMessage>();

        if (StringUtils.isEmpty(agencyData.getTripId())) {
            ErrorMessage error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_NO_MANDATORY_FIELDS, TemPropertyConstants.TravelAgencyAuditReportFields.TRIP_ID);
            errorMessages.add(error);
        }

        errorMessages.addAll(validateMissingAccountingInfo(agencyData));

        if (isAmountEmpty(agencyData.getTripExpenseAmount())) {
            ErrorMessage error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_NO_MANDATORY_FIELDS, TemPropertyConstants.TravelAgencyAuditReportFields.TRIP_EXPENSE_AMOUNT);
            errorMessages.add(error);
        }
        if (StringUtils.isEmpty(agencyData.getTripInvoiceNumber())) {
            ErrorMessage error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_NO_MANDATORY_FIELDS, TemPropertyConstants.TravelAgencyAuditReportFields.TRIP_INVOICE_NUMBER);
            errorMessages.add(error);
        }
        if (ObjectUtils.isNull(agencyData.getTransactionPostingDate())) {
            ErrorMessage error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_NO_MANDATORY_FIELDS, TemPropertyConstants.TravelAgencyAuditReportFields.TRANSACTION_POSTING_DATE);
            errorMessages.add(error);
        }
        if (isTripDataMissing(agencyData)) {
            ErrorMessage error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_AIR_LODGING_RENTAL_MISSING);
            errorMessages.add(error);
        }

        if (!errorMessages.isEmpty()) {
            LOG.error("Missing one or more required fields.");
        }

        return errorMessages;
    }

    /**
     * This method loops through the {@link TripAccountingInformation} and
     * verifies that each one has a Chart Code and Account Number.
     *
     * @param agencyData
     * @return
     */
    @Override
    public List<ErrorMessage> validateMissingAccountingInfo(AgencyStagingData agencyData) {

        List<ErrorMessage> errorMessages = new ArrayList<ErrorMessage>();

        List<TripAccountingInformation> accountingInfoList = agencyData.getTripAccountingInformation();

        if (accountingInfoList.isEmpty()) {
            ErrorMessage error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_REQUIRED_ACCOUNT_INFO);
            errorMessages.add(error);
        }

        return errorMessages;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.ExpenseImportByTripService#validateAgencyData(org.kuali.kfs.module.tem.businessobject.AgencyStagingData)
     */
    @Override
    public List<ErrorMessage> validateAgencyData(AgencyStagingData agencyData) {
        LOG.info("Validating agency data. tripId: " + agencyData.getTripId());

        List<ErrorMessage> errorMessages = validateMandatoryFieldsPresent(agencyData);
        if (!errorMessages.isEmpty()) {
            agencyData.setErrorCode(TemConstants.AgencyStagingDataErrorCodes.AGENCY_REQUIRED_FIELDS);
            return errorMessages;
        }

        errorMessages = validateDuplicateData(agencyData);
        if (!errorMessages.isEmpty()) {
            agencyData.setErrorCode(TemConstants.AgencyStagingDataErrorCodes.AGENCY_DUPLICATE_DATA);
            return errorMessages;
        }

        agencyData.setErrorCode(AgencyStagingDataErrorCodes.AGENCY_NO_ERROR);

        errorMessages = validateTripId(agencyData);

        errorMessages.addAll(validateAccountingInfo(agencyData));

        errorMessages.addAll(validateCreditCardAgency(agencyData));

        errorMessages.addAll(validateDistributionCode(agencyData));

        LOG.info("Finished validating agency data. tripId:" + agencyData.getTripId());
        agencyData.setProcessingTimestamp(dateTimeService.getCurrentTimestamp());
        if (ObjectUtils.isNull(agencyData.getCreationTimestamp())) {
            agencyData.setCreationTimestamp(dateTimeService.getCurrentTimestamp());
        }
        return errorMessages;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.ExpenseImportByTripService#validateTripId(org.kuali.kfs.module.tem.businessobject.AgencyStagingData)
     */
    @Override
    public List<ErrorMessage> validateTripId(AgencyStagingData agencyData) {

        List<ErrorMessage> errorMessages = new ArrayList<ErrorMessage>();

        TravelDocument travelDocument = getTravelDocumentService().getParentTravelDocument(agencyData.getTripId());
        if (ObjectUtils.isNotNull(travelDocument)) {
            return errorMessages;
        }

        LOG.error("Unable to retrieve a travel document for tripId: " + agencyData.getTripId());
        setErrorCode(agencyData, AgencyStagingDataErrorCodes.AGENCY_INVALID_TRIPID);
        errorMessages.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_INVALID_TRIP_ID));

        return errorMessages;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.ExpenseImportByTripService#validateAccountingInfo(org.kuali.kfs.module.tem.businessobject.TemProfile, org.kuali.kfs.module.tem.businessobject.AgencyStagingData)
     */
    @Override
    public List<ErrorMessage> validateAccountingInfo(AgencyStagingData agencyData) {

        List<ErrorMessage> errorMessages = new ArrayList<ErrorMessage>();

        // Get ACCOUNTING_LINE_VALIDATION parameter to determine which fields to validate
        Collection<String> validationParameters = getParameterService().getParameterValuesAsString(AgencyDataImportStep.class, TravelParameters.ACCOUNTING_LINE_VALIDATION);
        //don't need to validate any accounting information
        if (ObjectUtils.isNull(validationParameters)) {
            return errorMessages;
        }

        List<TripAccountingInformation> accountingInfo = agencyData.getTripAccountingInformation();
        for (TripAccountingInformation account : accountingInfo) {

            errorMessages.addAll(validateAccountingInfoLine(agencyData, account, validationParameters).values());
        }

        return errorMessages;
    }

    /**
     * This method is called by the AgencyStagingData maintainable for validating the new accounting line.
     * It won't need to set the error codes on the agency staging data object
     *
     * @see org.kuali.kfs.module.tem.batch.service.ExpenseImportByTripService#validateAccountingInfo(org.kuali.kfs.module.tem.businessobject.TripAccountingInformation)
     */
    @Override
    public Map<String, ErrorMessage> validateAccountingInfoLine(TripAccountingInformation accountingLine) {
        return validateAccountingInfoLine(null, accountingLine, null);
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.ExpenseImportByTripService#validateAccountingInfo(org.kuali.kfs.module.tem.businessobject.TripAccountingInformation)
     */
    protected Map<String, ErrorMessage> validateAccountingInfoLine(AgencyStagingData agencyData, TripAccountingInformation accountingLine, Collection<String> validationParameters) {
        Map<String, ErrorMessage> errorMap = new HashMap<String, ErrorMessage>();

        if (ObjectUtils.isNull(validationParameters)) {
            // Get ACCOUNTING_LINE_VALIDATION parameter to determine which fields to validate
            validationParameters = getParameterService().getParameterValuesAsString(AgencyDataImportStep.class, TravelParameters.ACCOUNTING_LINE_VALIDATION);

            //don't need to validate any accounting information
            if (ObjectUtils.isNull(validationParameters)) {
                return errorMap;
            }
        }

        boolean setAgencyDataErrorCode = false;

        String tripId = "";
        if (ObjectUtils.isNotNull(agencyData)) {
            setAgencyDataErrorCode = true;
            tripId = agencyData.getTripId();
        }

        ErrorMessage error = null;

        if (validationParameters.contains(AgencyStagingDataValidation.VALIDATE_ACCOUNT)) {
            if (!isAccountNumberValid(accountingLine.getTripChartCode(), accountingLine.getTripAccountNumber())) {

                if (setAgencyDataErrorCode) {
                    LOG.error("Invalid Account in Agency Data record. tripId: " + tripId + " chart code: " + accountingLine.getTripChartCode() + " account: " + accountingLine.getTripAccountNumber());
                    setErrorCode(agencyData, AgencyStagingDataErrorCodes.AGENCY_INVALID_ACCOUNT);
                }

                error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_INVALID_ACCOUNT_NUM, accountingLine.getTripChartCode(), accountingLine.getTripAccountNumber());
                errorMap.put(TemPropertyConstants.TravelAgencyAuditReportFields.TRIP_ACCOUNT_NUMBER, error);
            }
        }

        if (validationParameters.contains(AgencyStagingDataValidation.VALIDATE_SUBACCOUNT)) {
            // sub account is optional
            if (StringUtils.isNotEmpty(accountingLine.getTripSubAccountNumber()) &&
                !isSubAccountNumberValid(accountingLine.getTripChartCode(), accountingLine.getTripAccountNumber(), accountingLine.getTripSubAccountNumber())) {

                if (setAgencyDataErrorCode) {
                    LOG.error("Invalid SubAccount in Agency Data record. tripId: " + tripId + " chart code: " + accountingLine.getTripChartCode() + " account: " + accountingLine.getTripAccountNumber() + " subaccount: " + accountingLine.getTripSubAccountNumber());
                    setErrorCode(agencyData, AgencyStagingDataErrorCodes.AGENCY_INVALID_SUBACCOUNT);
                }
                error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_INVALID_SUBACCOUNT, accountingLine.getTripSubAccountNumber());
                errorMap.put(TemPropertyConstants.TravelAgencyAuditReportFields.TRIP_SUBACCOUNT_NUMBER, error);
            }
        }

        // project code is optional
        if (StringUtils.isNotEmpty(accountingLine.getProjectCode()) &&
            !isProjectCodeValid(accountingLine.getProjectCode())) {

            if (setAgencyDataErrorCode) {
                LOG.error("Invalid Project in Agency Data record. tripId: " + tripId + " project code: " + accountingLine.getProjectCode());
                setErrorCode(agencyData, AgencyStagingDataErrorCodes.AGENCY_INVALID_PROJECT);
            }
            error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_INVALID_PROJECT_CODE, accountingLine.getProjectCode());
            errorMap.put(TemPropertyConstants.TravelAgencyAuditReportFields.TRIP_PROJECT_CODE, error);
        }

        return errorMap;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.ExpenseImportByTripService#isDuplicate(org.kuali.kfs.module.tem.businessobject.AgencyStagingData)
     */
    @Override
    public List<ErrorMessage> validateDuplicateData(AgencyStagingData agencyData) {

        //make sure all the mandatory fields are present before checking for duplicates
        List<ErrorMessage> errorMessages = validateMandatoryFieldsPresent(agencyData);
        if (!errorMessages.isEmpty()) {
            return errorMessages;
        }

        List<AgencyStagingData> agencyDataList = checkForDuplicates(agencyData);
        if (ObjectUtils.isNotNull(agencyDataList) && !agencyDataList.isEmpty()) {

            boolean isDuplicate = false;
            String errorMessage = "Found a duplicate entry for Agency Staging Data: Duplicate Ids ";

            for (AgencyStagingData duplicate : agencyDataList) {
                Integer duplicateId = duplicate.getId();
                if (ObjectUtils.isNotNull(agencyData.getId()) && (agencyData.getId().intValue() != duplicateId.intValue())) {
                    errorMessage += duplicate.getId() + " ";
                    isDuplicate = true;
                }
            }
            if (isDuplicate) {
                LOG.error(errorMessage);

                ErrorMessage error = new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_TRIP_DUPLICATE_RECORD, agencyData.getTripId(), agencyData.getAgency(),
                    agencyData.getTransactionPostingDate().toString(), agencyData.getTripExpenseAmount().toString(), agencyData.getItineraryDataString());
                errorMessages.add(error);
            }
        }

        return errorMessages;
    }

    protected List<AgencyStagingData> checkForDuplicates(AgencyStagingData agencyStagingData) {
        // Verify that this isn't a duplicate entry based on the following:
        // Trip ID, Ticket Number or Itinerary Number, Agency Name, Transaction Date, Transaction Amount

        Map<String, Object> fieldValues = new HashMap<String, Object>();

        if (StringUtils.isNotEmpty(agencyStagingData.getTripId())) {
            fieldValues.put(TemPropertyConstants.TRIP_ID, agencyStagingData.getTripId());
        }
        if (StringUtils.isNotEmpty(agencyStagingData.getCreditCardOrAgencyCode())) {
            fieldValues.put(TemPropertyConstants.CREDIT_CARD_AGENCY_CODE, agencyStagingData.getCreditCardOrAgencyCode());
        }
        if (ObjectUtils.isNotNull(agencyStagingData.getTransactionPostingDate())) {
            fieldValues.put(TemPropertyConstants.TRANSACTION_POSTING_DATE, agencyStagingData.getTransactionPostingDate());
        }
        if (ObjectUtils.isNotNull(agencyStagingData.getTripExpenseAmount())) {
            fieldValues.put(TemPropertyConstants.TRIP_EXPENSE_AMOUNT, agencyStagingData.getTripExpenseAmount());
        }
        if (StringUtils.isNotEmpty(agencyStagingData.getAirTicketNumber())) {
            fieldValues.put(TemPropertyConstants.AIR_TICKET_NUMBER, agencyStagingData.getAirTicketNumber());
        }
        if (StringUtils.isNotEmpty(agencyStagingData.getLodgingItineraryNumber())) {
            fieldValues.put(TemPropertyConstants.LODGING_ITINERARY_NUMBER, agencyStagingData.getLodgingItineraryNumber());
        }
        if (StringUtils.isNotEmpty(agencyStagingData.getRentalCarItineraryNumber())) {
            fieldValues.put(TemPropertyConstants.RENTAL_CAR_ITINERARY_NUMBER, agencyStagingData.getRentalCarItineraryNumber());
        }

        List<AgencyStagingData> agencyDataList = (List<AgencyStagingData>) businessObjectService.findMatching(AgencyStagingData.class, fieldValues);
        return agencyDataList;
    }

    @Override
    public List<ErrorMessage> validateCreditCardAgency(AgencyStagingData agencyData) {
        List<ErrorMessage> errorMessages = new ArrayList<ErrorMessage>();

        if (!isCreditCardAgencyValid(agencyData)) {
            //setErrorCode is already done in isCreditCardAgencyValid()
            errorMessages.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_CREDIT_CARD_DATA_INVALID_CCA, agencyData.getCreditCardOrAgencyCode()));
        }

        return errorMessages;
    }

    @Override
    public List<ErrorMessage> validateDistributionCode(AgencyStagingData agencyData) {
        List<ErrorMessage> errorMessages = new ArrayList<ErrorMessage>();

        String distributionCode = agencyData.getDistributionCode();
        if (ObjectUtils.isNotNull(distributionCode)) {
            AgencyServiceFee serviceFee = getAgencyServiceFee(distributionCode);
            if (ObjectUtils.isNull(serviceFee)) {
                LOG.error("Invalid Distribution Code: " + distributionCode);
                setErrorCode(agencyData, AgencyStagingDataErrorCodes.AGENCY_INVALID_DI_CD);
                errorMessages.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_INVALID_DISTRIBUTION_CODE, distributionCode));
            }
        }

        return errorMessages;
    }

    /**
     * @see org.kuali.kfs.module.tem.batch.service.ExpenseImportByTripService#reconciliateExpense(org.kuali.kfs.module.tem.businessobject.AgencyStagingData, org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper)
     */
    @SuppressWarnings("null")
    @Override
    public List<ErrorMessage> reconciliateExpense(AgencyStagingData agencyData, GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {

        LOG.info("Reconciling expense for agency data: " + agencyData.getId() + " tripId: " + agencyData.getTripId());

        List<ErrorMessage> errors = new ArrayList<ErrorMessage>();

        //only reconcile the record if it is active
        if (agencyData.isActive()) {

            if (AgencyStagingDataErrorCodes.AGENCY_NO_ERROR.equals(agencyData.getErrorCode())) {

                TemConstants.ExpenseTypeMetaCategory expenseTypeCategory = agencyData.getExpenseTypeCategory();

                // This is the "match process" - see if there's credit card data that matches the agency data
                CreditCardStagingData ccData = null;
                if (expenseTypeCategory == TemConstants.ExpenseTypeMetaCategory.AIRFARE) {
                    // see if there's a CC that matches ticket number, service fee number, amount
                    ccData = travelExpenseService.findImportedCreditCardExpense(agencyData.getTripExpenseAmount(), agencyData.getAirTicketNumber(), agencyData.getAirServiceFeeNumber());
                } else if (expenseTypeCategory == TemConstants.ExpenseTypeMetaCategory.LODGING) {
                    // see if there's a CC that matches lodging itinerary number and amount
                    ccData = travelExpenseService.findImportedCreditCardExpense(agencyData.getTripExpenseAmount(), agencyData.getLodgingItineraryNumber());
                } else if (expenseTypeCategory == TemConstants.ExpenseTypeMetaCategory.RENTAL_CAR) {
                    // see if there's a CC that matches rental car itinerary number and amount
                    ccData = travelExpenseService.findImportedCreditCardExpense(agencyData.getTripExpenseAmount(), agencyData.getRentalCarItineraryNumber());
                }

                if (ObjectUtils.isNotNull(ccData)) {
                    LOG.info("Found a match for Agency: " + agencyData.getId() + " Credit Card: " + ccData.getId() + " tripId: " + agencyData.getTripId());

                    final TravelDocument travelDocument = getTravelDocumentService().getParentTravelDocument(agencyData.getTripId());

                    ExpenseTypeObjectCode travelExpenseType = getTravelExpenseType(expenseTypeCategory, travelDocument);
                    if (travelExpenseType != null) {

                        if (isDocumentStatusValidForReconcilingCharges(agencyData)) {

                            if (isAccountingLinesMatch(agencyData)) {
                                List<TripAccountingInformation> accountingInfo = agencyData.getTripAccountingInformation();

                                HistoricalTravelExpense expense = travelExpenseService.createHistoricalTravelExpense(agencyData, ccData, travelExpenseType);
                                AgencyServiceFee serviceFee = getAgencyServiceFee(agencyData.getDistributionCode());

                                List<GeneralLedgerPendingEntry> entries = new ArrayList<GeneralLedgerPendingEntry>();

                                // Need to split up the amounts if there are multiple accounts
                                KualiDecimal remainingAmount = agencyData.getTripExpenseAmount();
                                KualiDecimal numAccounts = new KualiDecimal(accountingInfo.size());
                                KualiDecimal currentAmount = agencyData.getTripExpenseAmount().divide(numAccounts);
                                KualiDecimal remainingFeeAmount = new KualiDecimal(0);
                                KualiDecimal currentFeeAmount = new KualiDecimal(0);
                                if (ObjectUtils.isNotNull(serviceFee)) {
                                    remainingFeeAmount = serviceFee.getServiceFee();
                                    currentFeeAmount = serviceFee.getServiceFee().divide(numAccounts);
                                }

                                String creditObjectCode = getParameterService().getParameterValueAsString(TemParameterConstants.TEM_ALL.class, AgencyMatchProcessParameter.TRAVEL_CREDIT_CARD_CLEARING_OBJECT_CODE);

                                boolean allGlpesCreated = true;

                                for (int i = 0; i < accountingInfo.size(); i++) {
                                    TripAccountingInformation info = accountingInfo.get(i);

                                    // If its the last account, use the remainingAmount to resolve rounding
                                    if (i < accountingInfo.size() - 1) {
                                        remainingAmount = remainingAmount.subtract(currentAmount);
                                        remainingFeeAmount = remainingFeeAmount.subtract(currentFeeAmount);
                                    } else {
                                        currentAmount = remainingAmount;
                                        currentFeeAmount = remainingFeeAmount;
                                    }
                                    String objectCode = info.getObjectCode();
                                    if (StringUtils.isEmpty(objectCode)) {
                                        objectCode = travelExpenseType.getFinancialObjectCode();
                                    }

                                    // set the amount on the accounting info for documents pulling in imported expenses
                                    info.setAmount(currentAmount);

                                    if (ObjectUtils.isNotNull(serviceFee)) {
                                        // Agency Data has a DI Code that maps to an Agency Service Fee, create GLPEs for it.
                                        LOG.info("Processing Service Fee GLPE for agency: " + agencyData.getId() + " tripId: " + agencyData.getTripId());

                                        final boolean generateOffset = true;
                                        List<GeneralLedgerPendingEntry> pendingEntries = importedExpensePendingEntryService.buildDebitPendingEntry(agencyData, info, sequenceHelper, objectCode, currentFeeAmount, generateOffset);
                                        allGlpesCreated = importedExpensePendingEntryService.checkAndAddPendingEntriesToList(pendingEntries, entries, agencyData, false, generateOffset);

                                        pendingEntries = importedExpensePendingEntryService.buildServiceFeeCreditPendingEntry(agencyData, info, sequenceHelper, serviceFee, currentFeeAmount, generateOffset);
                                        allGlpesCreated &= importedExpensePendingEntryService.checkAndAddPendingEntriesToList(pendingEntries, entries, agencyData, true, generateOffset);
                                    }

                                    final boolean generateOffset = true;
                                    List<GeneralLedgerPendingEntry> pendingEntries = importedExpensePendingEntryService.buildDebitPendingEntry(agencyData, info, sequenceHelper, objectCode, currentAmount, generateOffset);
                                    allGlpesCreated &= importedExpensePendingEntryService.checkAndAddPendingEntriesToList(pendingEntries, entries, agencyData, false, generateOffset);

                                    pendingEntries = importedExpensePendingEntryService.buildCreditPendingEntry(agencyData, info, sequenceHelper, creditObjectCode, currentAmount, generateOffset);
                                    allGlpesCreated &= importedExpensePendingEntryService.checkAndAddPendingEntriesToList(pendingEntries, entries, agencyData, true, generateOffset);
                                }

                                if (entries.size() > 0 && allGlpesCreated) {

                                    GeneralLedgerPendingEntry glpe = entries.get(0);

                                    //add the GLPE document number to the historical expense entry
                                    expense.setDocumentNumber(glpe.getDocumentNumber());
                                    //add the TravelDocument document number to the historical expense entry
                                    expense.setDocumentType(travelDocument.getDocumentTypeName());
                                    //set the travel company to be the merchant name from the credit card data
                                    expense.setTravelCompany(ccData.getMerchantName());

                                    businessObjectService.save(entries);
                                    businessObjectService.save(expense);
                                    ccData.setErrorCode(CreditCardStagingDataErrorCodes.CREDIT_CARD_MOVED_TO_HISTORICAL);
                                    ccData.setMoveToHistoryIndicator(true);
                                    businessObjectService.save(ccData);
                                    agencyData.setMoveToHistoryIndicator(true);
                                    agencyData.setErrorCode(AgencyStagingDataErrorCodes.AGENCY_MOVED_TO_HISTORICAL);

                                    // nota bene: agency staging data object can NOT be saved here b/c the AgencyStagingDataMaint doc calls this method and will save it itself once processing completes.
                                    // batch steps which call this method need to save the business object after calling this method
                                } else {
                                    LOG.error("An error occurred while creating GLPEs for agency data: " + agencyData.getId() + ", tripId: " + agencyData.getTripId() + ". Will not reconcile expense.");
                                    errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_RECON_GLPE_CREATION));
                                }
                            } else {
                                LOG.error("The accounting lines on the agency data record do not have a match on the travel document. Agency data: " + agencyData.getId() + "; tripId: " + agencyData.getTripId() + "; documentNumber: " + travelDocument.getDocumentNumber());
                                errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_RECON_ACCOUNTING_LINE_MATCH, travelDocument.getDocumentNumber()));
                            }
                        } else {
                            LOG.error("The document has not been approved. Document Number: " + travelDocument.getDocumentNumber());
                            errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGECNY_DATA_RECON_DOCUMENT_STATUS, travelDocument.getDocumentNumber()));
                        }
                    } else {
                        LOG.info("No expense type object code could be found for agency data: " + agencyData.getId() + " tripId: " + agencyData.getTripId());
                        errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_RECON_EXPENSE_TYPE_OBJECT_CODE, expenseTypeCategory.getCode(), travelDocument.getDocumentTypeName(), travelDocument.getTripTypeCode(), travelDocument.getTraveler().getTravelerTypeCode()));
                    }
                } else {
                    LOG.info("No match found for agency data: " + agencyData.getId() + " tripId:" + agencyData.getTripId());

                    if (expenseTypeCategory == TemConstants.ExpenseTypeMetaCategory.AIRFARE) {
                        errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_RECON_TRIP_MATCH_AIR, agencyData.getTripExpenseAmount().toString(), agencyData.getAirTicketNumber(), agencyData.getAirServiceFeeNumber()));
                    } else if (expenseTypeCategory == TemConstants.ExpenseTypeMetaCategory.LODGING) {
                        errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_RECON_TRIP_MATCH, agencyData.getTripExpenseAmount().toString(), agencyData.getLodgingItineraryNumber()));
                    } else if (expenseTypeCategory == TemConstants.ExpenseTypeMetaCategory.RENTAL_CAR) {
                        errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_RECON_TRIP_MATCH, agencyData.getTripExpenseAmount().toString(), agencyData.getRentalCarItineraryNumber()));
                    } else {
                        errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_RECON_INVALID_EXPENSE_TYPE_CATEGORY));
                    }
                }
            } else {
                LOG.info("Agency Data: " + agencyData.getId() + "; expected errorCode=" + AgencyStagingDataErrorCodes.AGENCY_NO_ERROR + ", received errorCode=" + agencyData.getErrorCode() + ". Will not attempt to reconcile expense.");
                errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_RECON_INVALID_ERROR_CODE, AgencyStagingDataErrorCodes.AGENCY_NO_ERROR, agencyData.getErrorCode()));
            }
        } else {
            LOG.info("Agency Data: " + agencyData.getId() + ", is not active. Will not attempt to reconcile expense.");
            errors.add(new ErrorMessage(TemKeyConstants.MESSAGE_AGENCY_DATA_RECON_ACTIVE));
        }

        LOG.info("Finished reconciling expense for agency data: " + agencyData.getId() + ", Trip Id: " + agencyData.getTripId() + ". Agency data " + (errors.isEmpty() ? "was" : "was not") + " reconciled.");
        return errors;
    }

    /**
     * This method checks whether the accounting lines on the agency staging data record have a match with the source accounting lines on the travel document
     *
     * @param agencyDataAccountingLines
     * @param travelDocumentAccountingLines
     * @return
     */
    protected boolean isAccountingLinesMatch(AgencyStagingData agencyStagingData) {

        List<TripAccountingInformation> agencyDataAccountingLines = agencyStagingData.getTripAccountingInformation();

        //check that the accounting lines on the agency record have a matching source accounting line on the trip
        List<TemSourceAccountingLine> tripSourceAccountingLines = getSourceAccountingLinesByTrip(agencyStagingData.getTripId());

        // Get ACCOUNTING_LINE_VALIDATION parameter to determine which fields to validate
        Collection<String> validationParams = getParameterService().getParameterValuesAsString(AgencyDataImportStep.class, TravelParameters.ACCOUNTING_LINE_VALIDATION);
        if (ObjectUtils.isNull(validationParams)) {
            LOG.info("Did not find the parameter, " + TravelParameters.ACCOUNTING_LINE_VALIDATION + ". Will not validate matching accounting lines.");
            return true;
        }

        for (TripAccountingInformation agencyAccount : agencyDataAccountingLines) {

            boolean foundAMatch = false;
            String documentNumbers = "";
            for (TemSourceAccountingLine sourceAccountingLine : tripSourceAccountingLines) {

                boolean account = true, subaccount = true, objectcode = true, subobjectcode = true;

                if (validationParams.contains(TemConstants.AgencyStagingDataValidation.VALIDATE_ACCOUNT)) {
                    //need to clean the empty vs null data for comparison in StringUtils.equals
                    String agencyAccountNumber = StringUtils.trimToEmpty(agencyAccount.getTripAccountNumber());
                    String sourceAccountNumber = StringUtils.trimToEmpty(sourceAccountingLine.getAccountNumber());

                    if (!StringUtils.equals(agencyAccountNumber, sourceAccountNumber)) {
                        account = false;
                    }
                }
                if (validationParams.contains(TemConstants.AgencyStagingDataValidation.VALIDATE_SUBACCOUNT)) {
                    String agencySubAccountNumber = StringUtils.trimToEmpty(agencyAccount.getTripSubAccountNumber());
                    String sourceSubAccountNumber = StringUtils.trimToEmpty(sourceAccountingLine.getSubAccountNumber());

                    if (!StringUtils.equals(agencySubAccountNumber, sourceSubAccountNumber)) {
                        subaccount = false;
                    }
                }

                if (account && subaccount && objectcode && subobjectcode) {
                    LOG.debug("Found accounting line match on DocumentId: " + sourceAccountingLine.getDocumentNumber() + ", " + sourceAccountingLine);
                    foundAMatch = true;
                    break;
                } else {
                    documentNumbers += sourceAccountingLine.getDocumentNumber() + ",";
                }
            }
            //the agency data accounting line does not have a matching source account on the trip- return false
            if (!foundAMatch) {
                LOG.info("Agency accounting line did not match any document accounting lines: AgencyStgDataId: " + agencyAccount.getAgencyStagingDataId() + ",trpAcctInfoId: " + agencyAccount.getId() + ", documentNumbers:[" + documentNumbers + "]");
                return false;
            }
        }
        //all agency data accounting lines have matching source accounts on the trip- return true
        return true;
    }

    /**
     * This method gets the {@link TemTravelExpenseTypeCode} associated with the expense type and travel document
     *
     * @param expenseTypeParamCode
     * @param travelDocumentIdentifier
     * @return
     */
    protected ExpenseTypeObjectCode getTravelExpenseType(TemConstants.ExpenseTypeMetaCategory expenseCategory, TravelDocument travelDoc) {
        // get the default expense type for category
        final ExpenseType expenseType = getTravelExpenseService().getDefaultExpenseTypeForCategory(expenseCategory);

        // Is there a document associated with this trip?  If so, let's grab the trip type and traveler type from that
        if (ObjectUtils.isNotNull(travelDoc)) {

            ExpenseTypeObjectCode expenseTypeObjectCode = travelExpenseService.getExpenseType(expenseType.getCode(), travelDoc.getDocumentTypeName(), travelDoc.getTripTypeCode(), travelDoc.getTraveler().getTravelerTypeCode());
            if (expenseTypeObjectCode == null) {
                LOG.error("Unable to retrieve ExpenseTypeObjectCode for ExpenseTypeCode:" + expenseType.getCode() + ", DocTypeName:" + travelDoc.getDocumentTypeName() + ", TripType:" + travelDoc.getTripTypeCode() + ", TravelerType:" + travelDoc.getTraveler().getTravelerTypeCode());
            }

            return expenseTypeObjectCode;
        }
        LOG.error("Unable to retrieve TemTravelExpenseTypeCode");
        return null; // we shouldn't ever get here if the trip id was validated, but hey.  You never know.
    }

    /**
     * This method returns the {@link AgencyServiceFee} by distribution code (distributionCode).
     *
     * @param distributionCode
     * @return
     */
    protected AgencyServiceFee getAgencyServiceFee(String distributionCode) {
        if (StringUtils.isNotEmpty(distributionCode)) {
            Map<String, String> criteria = new HashMap<String, String>(1);
            criteria.put(TemPropertyConstants.DISTRIBUTION_CODE, distributionCode);
            criteria.put(TemPropertyConstants.ACTIVE_IND, TemConstants.YES);
            List<AgencyServiceFee> serviceFee = (List<AgencyServiceFee>) getBusinessObjectService().findMatching(AgencyServiceFee.class, criteria);
            if (ObjectUtils.isNotNull(serviceFee) && serviceFee.size() > 0) {
                return serviceFee.get(0);
            }
        }
        return null;
    }

    protected List<TemSourceAccountingLine> getSourceAccountingLinesByTrip(String travelDocumentIdentifier) {

        Collection<String> travelDocumentNumbers = getTravelDocumentService().getApprovedTravelDocumentNumbersByTrip(travelDocumentIdentifier);
        LOG.info("Will attempt to retrieve source accounting lines for the following documents: " + travelDocumentNumbers);

        ArrayList temSourceAccountingLines = new ArrayList<TemSourceAccountingLine>();

        if (!travelDocumentNumbers.isEmpty()) {

            Map<String, Object> fieldValues = new HashMap<String, Object>();
            fieldValues.put(KFSPropertyConstants.DOCUMENT_NUMBER, travelDocumentNumbers);
            fieldValues.put(KFSPropertyConstants.FINANCIAL_DOCUMENT_LINE_TYPE_CODE, KFSConstants.SOURCE_ACCT_LINE_TYPE_CODE);

            temSourceAccountingLines.addAll(getBusinessObjectService().findMatchingOrderBy(TemSourceAccountingLine.class, fieldValues, KFSPropertyConstants.SEQUENCE_NUMBER, true));
        }

        return temSourceAccountingLines;
    }

    protected boolean isDocumentStatusValidForReconcilingCharges(AgencyStagingData agencyData) {

        TravelDocument parentDocument = getTravelDocumentService().getParentTravelDocument(agencyData.getTripId());
        return getTravelDocumentService().isDocumentStatusValidForReconcilingCharges(parentDocument);
    }

    /**
     * Gets the travelAuthorizationService attribute.
     *
     * @return Returns the travelAuthorizationService.
     */
    public TravelAuthorizationService getTravelAuthorizationService() {
        return travelAuthorizationService;
    }

    /**
     * Sets the travelAuthorizationService attribute value.
     *
     * @param travelAuthorizationService The travelAuthorizationService to set.
     */
    public void setTravelAuthorizationService(TravelAuthorizationService travelAuthorizationService) {
        this.travelAuthorizationService = travelAuthorizationService;
    }

    /**
     * Gets the temProfileService attribute.
     *
     * @return Returns the temProfileService.
     */
    public TemProfileService getTemProfileService() {
        return temProfileService;
    }

    /**
     * Sets the temProfileService attribute value.
     *
     * @param temProfileService The temProfileService to set.
     */
    public void setTemProfileService(TemProfileService temProfileService) {
        this.temProfileService = temProfileService;
    }

    /**
     * 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;
    }

    /**
     * Gets the travelExpenseService attribute.
     *
     * @return Returns the travelExpenseService.
     */
    public TravelExpenseService getTravelExpenseService() {
        return travelExpenseService;
    }

    /**
     * Sets the travelExpenseService attribute value.
     *
     * @param travelExpenseService The travelExpenseService to set.
     */
    public void setTravelExpenseService(TravelExpenseService travelExpenseService) {
        this.travelExpenseService = travelExpenseService;
    }

    public ImportedExpensePendingEntryService getImportedExpensePendingEntryService() {
        return importedExpensePendingEntryService;
    }

    public void setImportedExpensePendingEntryService(ImportedExpensePendingEntryService importedExpensePendingEntryService) {
        this.importedExpensePendingEntryService = importedExpensePendingEntryService;
    }

    public TravelDocumentService getTravelDocumentService() {
        return this.travelDocumentService;
    }

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

    public TravelEncumbranceService getTravelEncumbranceService() {
        return this.travelEncumbranceService;
    }

    public void setTravelEncumbranceService(TravelEncumbranceService travelEncumbranceService) {
        this.travelEncumbranceService = travelEncumbranceService;
    }

}
