/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 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 java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.kuali.kfs.module.tem.TemConstants; 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.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.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.kuali.rice.krad.service.BusinessObjectService; import org.kuali.rice.krad.util.ErrorMessage; import org.kuali.rice.krad.util.ObjectUtils; import org.springframework.transaction.annotation.Transactional; /** * 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; } }