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

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.purap.PurapConstants;
import org.kuali.kfs.module.purap.PurapConstants.PurchaseOrderStatuses;
import org.kuali.kfs.module.purap.PurapKeyConstants;
import org.kuali.kfs.module.purap.PurapParameterConstants;
import org.kuali.kfs.module.purap.batch.ElectronicInvoiceStep;
import org.kuali.kfs.module.purap.businessobject.ElectronicInvoice;
import org.kuali.kfs.module.purap.businessobject.ElectronicInvoiceDetailRequestSummary;
import org.kuali.kfs.module.purap.businessobject.ElectronicInvoiceRejectReason;
import org.kuali.kfs.module.purap.businessobject.ElectronicInvoiceRejectReasonType;
import org.kuali.kfs.module.purap.businessobject.PurApItem;
import org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem;
import org.kuali.kfs.module.purap.document.PurchaseOrderDocument;
import org.kuali.kfs.module.purap.service.ElectronicInvoiceMatchingService;
import org.kuali.kfs.module.purap.util.ElectronicInvoiceUtils;
import org.kuali.kfs.module.purap.util.PurApItemUtils;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.TaxService;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.kfs.vnd.businessobject.PurchaseOrderCostSource;
import org.kuali.kfs.vnd.businessobject.VendorDetail;
import org.kuali.kfs.vnd.document.service.VendorService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ElectronicInvoiceMatchingServiceImpl implements ElectronicInvoiceMatchingService {

    private Logger LOG = Logger.getLogger(ElectronicInvoiceMatchingServiceImpl.class);

    private Map<String, ElectronicInvoiceRejectReasonType> rejectReasonTypes;
    private VendorService vendorService;
    private TaxService taxService;
    private DateTimeService dateTimeService;

    String upperVariancePercentString;
    String lowerVariancePercentString;

    @Override
    public void doMatchingProcess(ElectronicInvoiceOrderHolder orderHolder) {

        if (LOG.isInfoEnabled()) {
            LOG.info("Matching process started");
        }

        upperVariancePercentString = SpringContext.getBean(ParameterService.class).getParameterValueAsString(ElectronicInvoiceStep.class, PurapParameterConstants.ElectronicInvoiceParameters.SALES_TAX_UPPER_VARIANCE_PERCENT);
        lowerVariancePercentString = SpringContext.getBean(ParameterService.class).getParameterValueAsString(ElectronicInvoiceStep.class, PurapParameterConstants.ElectronicInvoiceParameters.SALES_TAX_LOWER_VARIANCE_PERCENT);
        ;

        try {
            if (orderHolder.isValidateHeaderInformation()) {

                validateHeaderInformation(orderHolder);

                if (orderHolder.isInvoiceRejected()) {
                    if (LOG.isInfoEnabled()) {
                        LOG.info("Matching process failed at header validation");
                    }
                    return;
                }
            }

            validateInvoiceDetails(orderHolder);

            if (orderHolder.isInvoiceRejected()) {
                if (LOG.isInfoEnabled()) {
                    LOG.info("Matching process failed at order detail validation");
                }
                return;
            }

        } catch (NumberFormatException e) {
            if (LOG.isInfoEnabled()) {
                LOG.info("Matching process matching failed due to number format exception " + e.getMessage());
            }
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVALID_NUMBER_FORMAT, e.getMessage(), orderHolder.getFileName());
            orderHolder.addInvoiceHeaderRejectReason(rejectReason);
            return;
        }

        if (LOG.isInfoEnabled()) {
            LOG.info("Matching process ended successfully");
        }
    }

    protected void validateHeaderInformation(ElectronicInvoiceOrderHolder orderHolder) {

        String dunsField = PurapConstants.ElectronicInvoice.RejectDocumentFields.VENDOR_DUNS_NUMBER;
        String applnResourceKeyName = PurapKeyConstants.ERROR_REJECT_INVALID_DUNS;

        if (StringUtils.isEmpty(orderHolder.getDunsNumber())) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUNS_NOT_FOUND, null, orderHolder.getFileName());
            orderHolder.addInvoiceHeaderRejectReason(rejectReason, dunsField, applnResourceKeyName);
            return;
        }

        if (orderHolder.isRejectDocumentHolder()) {
            VendorDetail vendorDetail = SpringContext.getBean(VendorService.class).getVendorByDunsNumber(orderHolder.getDunsNumber());
            if (vendorDetail == null) {
                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUNS_INVALID, null, orderHolder.getFileName());
                orderHolder.addInvoiceHeaderRejectReason(rejectReason, dunsField, applnResourceKeyName);
                return;
            }
        } else {
            if (orderHolder.getVendorHeaderId() == null && orderHolder.getVendorDetailId() == null) {
                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUNS_INVALID, null, orderHolder.getFileName());
                orderHolder.addInvoiceHeaderRejectReason(rejectReason, dunsField, applnResourceKeyName);
                return;
            }
        }

        String invoiceNumberField = PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_FILE_NUMBER;
        if (!orderHolder.isInvoiceNumberAcceptIndicatorEnabled()) {
            if (StringUtils.isEmpty(orderHolder.getInvoiceNumber())) {
                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_ID_EMPTY, null, orderHolder.getFileName());
                orderHolder.addInvoiceHeaderRejectReason(rejectReason, invoiceNumberField, PurapKeyConstants.ERROR_REJECT_INVOICE_NUMBER_EMPTY);
                return;
            }
        }

        String invoiceDateField = PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_FILE_DATE;

        if (StringUtils.isEmpty(orderHolder.getInvoiceDateString()) || orderHolder.getInvoiceDate() == null) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_DATE_INVALID, null, orderHolder.getFileName());
            orderHolder.addInvoiceHeaderRejectReason(rejectReason, invoiceDateField, PurapKeyConstants.ERROR_REJECT_INVOICE_DATE_INVALID);
            return;
        } else if (orderHolder.getInvoiceDate().after(dateTimeService.getCurrentDate())) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_DATE_GREATER, null, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, invoiceDateField, PurapKeyConstants.ERROR_REJECT_INVOICE_DATE_GREATER);
            return;
        }

        if (orderHolder.isInformationOnly()) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INFORMATION_ONLY, null, orderHolder.getFileName());
            orderHolder.addInvoiceHeaderRejectReason(rejectReason);
            return;
        }

        validateSummaryAmounts(orderHolder);

        if (orderHolder.isInvoiceRejected()) {
            return;
        }

        validateItemTypes(orderHolder);

        if (orderHolder.isInvoiceRejected()) {
            return;
        }

    }

    protected void validateSummaryAmounts(ElectronicInvoiceOrderHolder orderHolder) {

        if (orderHolder.isRejectDocumentHolder()) {
            /**
             * If there are any rejects related to the summary, we're retaining it since
             * it's not possible to get the summary amount totals from the reject doc
             */
            return;
        }

        ElectronicInvoiceDetailRequestSummary summary = orderHolder.getElectronicInvoice().getInvoiceDetailRequestSummary();

        boolean enableSalesTaxInd = SpringContext.getBean(ParameterService.class).getParameterValueAsBoolean(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_SALES_TAX_IND);

        boolean salesTaxUsed = false;
        PurchaseOrderDocument poDoc = orderHolder.getPurchaseOrderDocument();
        if (poDoc != null) {  // we handle bad PO's in the eInvoice later, so just skip this
            List<PurApItem> items = PurApItemUtils.getAboveTheLineOnly(poDoc.getItems());
            for (PurApItem item : items) {
                if (item.getItemType().isTaxableIndicator()) {
                    salesTaxUsed = true;
                    break;
                }
            }

            boolean useTaxUsed = poDoc.isUseTaxIndicator();
            enableSalesTaxInd &= (salesTaxUsed || useTaxUsed);

            BigDecimal summaryTaxAmount = summary.getInvoiceTaxAmount();
            if (!enableSalesTaxInd) {
                // if sales tax is disabled, total tax amount shall be zero
                if (summaryTaxAmount.compareTo(new BigDecimal(0)) != 0) {
                    String extraDescription = "Summary Tax Amount:" + summaryTaxAmount;
                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.TAX_SUMMARY_AMT_EXISTS, extraDescription, orderHolder.getFileName());
                    orderHolder.addInvoiceHeaderRejectReason(rejectReason);
                }
            } else if (orderHolder.isTaxInLine()) {
                validateSummaryAmount(orderHolder, summaryTaxAmount, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_TAX, PurapConstants.ElectronicInvoice.TAX_SUMMARY_AMT_MISMATCH);
            }
        }

        if (orderHolder.isShippingInLine()) {
            validateSummaryAmount(orderHolder, summary.getInvoiceShippingAmount(), ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SHIPPING, PurapConstants.ElectronicInvoice.SHIPPING_SUMMARY_AMT_MISMATCH);
        }

        if (orderHolder.isSpecialHandlingInLine()) {
            validateSummaryAmount(orderHolder, summary.getInvoiceSpecialHandlingAmount(), ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SPECIAL_HANDLING, PurapConstants.ElectronicInvoice.SPL_HANDLING_SUMMARY_AMT_MISMATCH);
        }

        if (orderHolder.isDiscountInLine()) {
            validateSummaryAmount(orderHolder, summary.getInvoiceDiscountAmount(), ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_DISCOUNT, PurapConstants.ElectronicInvoice.DISCOUNT_SUMMARY_AMT_MISMATCH);
        }

    }

    protected void validateSummaryAmount(ElectronicInvoiceOrderHolder orderHolder,
                                         BigDecimal summaryAmount,
                                         String invoiceLineItemTypeCode,
                                         String rejectDescriptionCode) {

        BigDecimal lineItemTotalAmount = orderHolder.getElectronicInvoice().getFileTotalAmountForInLineItems(invoiceLineItemTypeCode);

//        if (lineItemTotalAmount.compareTo(BigDecimal.ZERO) != 0) { // old way, but it's not needed
        if ((lineItemTotalAmount.compareTo(summaryAmount)) != 0) {
            String extraDescription = "Line Total Amount:" + lineItemTotalAmount + ",Summary Total Amount:" + summaryAmount;
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(rejectDescriptionCode, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceHeaderRejectReason(rejectReason);
        }
//        }
    }

    protected void validateItemTypes(ElectronicInvoiceOrderHolder orderHolder) {

        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_ITEM);
        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_TAX);
        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SHIPPING);
        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SPECIAL_HANDLING);
        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_DISCOUNT);
        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_EXMT);

    }

    protected void validateItemMapping(ElectronicInvoiceOrderHolder orderHolder, String kualiItemTypeCode) {

        if (!orderHolder.isItemTypeAvailableInItemMapping(kualiItemTypeCode)) {
            String extraDescription = kualiItemTypeCode;
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.ITEM_MAPPING_NOT_AVAILABLE, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceHeaderRejectReason(rejectReason);
            return;
        }

    }

    protected void validateInvoiceDetails(ElectronicInvoiceOrderHolder orderHolder) {

        validatePurchaseOrderMatch(orderHolder);

        if (orderHolder.isInvoiceRejected()) {
            return;
        }

        validateInvoiceItems(orderHolder);

        if (LOG.isInfoEnabled()) {
            if (!orderHolder.isInvoiceRejected()) {
                LOG.info("Purchase order document match done successfully");
            }
        }
    }

    protected void validatePurchaseOrderMatch(ElectronicInvoiceOrderHolder orderHolder) {

        String poIDFieldName = PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_PO_ID;
        String poID = orderHolder.getInvoicePurchaseOrderID();

        if (StringUtils.isEmpty(poID)) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ID_EMPTY, null, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, poIDFieldName, PurapKeyConstants.ERROR_REJECT_INVOICE_POID_EMPTY);
            return;
        }

        String extraDesc = "Invoice Order ID:" + poID;

        if (!NumberUtils.isDigits(poID)) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ID_INVALID_FORMAT, extraDesc, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, poIDFieldName, PurapKeyConstants.ERROR_REJECT_INVOICE_POID_INVALID);
            return;
        }

        PurchaseOrderDocument poDoc = orderHolder.getPurchaseOrderDocument();

        if (poDoc == null) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_NOT_EXISTS, extraDesc, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, poIDFieldName, PurapKeyConstants.ERROR_REJECT_INVOICE__PO_NOT_EXISTS);
            return;
        }

        if (!poDoc.getApplicationDocumentStatus().equals(PurchaseOrderStatuses.APPDOC_OPEN)) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_NOT_OPEN, null, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, poIDFieldName, PurapKeyConstants.ERROR_REJECT_INVOICE_PO_CLOSED);
            return;
        }

        if (poDoc.getVendorHeaderGeneratedIdentifier() == null ||
            poDoc.getVendorDetailAssignedIdentifier() == null ||
            !(poDoc.getVendorHeaderGeneratedIdentifier().equals(orderHolder.getVendorHeaderId()) &&
                poDoc.getVendorDetailAssignedIdentifier().equals(orderHolder.getVendorDetailId()))) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_VENDOR_NOT_MATCHES_WITH_INVOICE_VENDOR, null, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason);
            return;
        }

    }

    protected void validateInvoiceItems(ElectronicInvoiceOrderHolder orderHolder) {

        Set poLineNumbers = new HashSet();

        ElectronicInvoiceItemHolder[] itemHolders = orderHolder.getItems();
        if (itemHolders != null) {
            for (int i = 0; i < itemHolders.length; i++) {
                validateInvoiceItem(itemHolders[i], poLineNumbers);
            }
        }
    }

    protected void validateInvoiceItem(ElectronicInvoiceItemHolder itemHolder,
                                       Set poLineNumbers) {

        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();

        if (poItem == null) {
            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.NO_MATCHING_PO_ITEM, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_INVOICE__ITEM_NOMATCH);
            return;
        }

        if (poLineNumbers.contains(itemHolder.getInvoiceItemLineNumber())) {
            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUPLIATE_INVOICE_LINE_ITEM, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_PO_ITEM_DUPLICATE);
            return;
        } else {
            poLineNumbers.add(itemHolder.getInvoiceItemLineNumber());
        }

        if (!poItem.isItemActiveIndicator()) {
            String extraDescription = "PO Item Line Number:" + poItem.getItemLineNumber();
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INACTIVE_LINE_ITEM, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_PO_ITEM_INACTIVE);
            return;
        }

        if (!itemHolder.isCatalogNumberAcceptIndicatorEnabled()) {
            validateCatalogNumber(itemHolder);
            if (orderHolder.isInvoiceRejected()) {
                return;
            }
        }

        if (!itemHolder.isUnitOfMeasureAcceptIndicatorEnabled()) {
            if (!StringUtils.equals(poItem.getItemUnitOfMeasureCode(), itemHolder.getInvoiceItemUnitOfMeasureCode())) {
                String extraDescription = "Invoice UOM:" + itemHolder.getInvoiceItemUnitOfMeasureCode() + ", PO UOM:" + poItem.getItemUnitOfMeasureCode();
                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.UNIT_OF_MEASURE_MISMATCH, extraDescription, orderHolder.getFileName());
                orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_UOM, PurapKeyConstants.ERROR_REJECT_UOM_MISMATCH);
                return;
            }
        }

        validateUnitPrice(itemHolder);

        if (orderHolder.isInvoiceRejected()) {
            return;
        }

        validateSalesTax(itemHolder);

        if (orderHolder.isInvoiceRejected()) {
            return;
        }

        if (poItem.getItemQuantity() != null) {
            validateQtyBasedItem(itemHolder);
        } else {
            validateNonQtyBasedItem(itemHolder);
        }

    }

    protected void validateCatalogNumber(ElectronicInvoiceItemHolder itemHolder) {

        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();

        String invoiceCatalogNumberStripped = itemHolder.getCatalogNumberStripped();
        String poCatalogNumberStripped = ElectronicInvoiceUtils.stripSplChars(poItem.getItemCatalogNumber());

        /**
         * If Catalog number in invoice and po are not empty, create reject reason if it doesn't match
         */
        if (StringUtils.isNotBlank(invoiceCatalogNumberStripped) &&
            StringUtils.isNotBlank(poCatalogNumberStripped)) {

            if (!StringUtils.equals(poCatalogNumberStripped, invoiceCatalogNumberStripped)) {

                String extraDescription = "Invoice Catalog No:" + invoiceCatalogNumberStripped + ", PO Catalog No:" + poCatalogNumberStripped;
                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.CATALOG_NUMBER_MISMATCH, extraDescription, orderHolder.getFileName());
                orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_CATALOG_NUMBER, PurapKeyConstants.ERROR_REJECT_CATALOG_MISMATCH);
            }

        } else {

            /**
             * If catalog number is empty in PO/&Invoice, check whether the catalog check is required for the requisition source.
             * If exists in param, create reject reason.
             * If not exists, continue with UOM and unit price match.
             */
            String reqSourceRequiringCatalogMatch = SpringContext.getBean(ParameterService.class).getParameterValueAsString(ElectronicInvoiceStep.class, PurapParameterConstants.ElectronicInvoiceParameters.REQUISITION_SOURCES_REQUIRING_CATALOG_MATCHING);
            String requisitionSourceCodeInPO = orderHolder.getPurchaseOrderDocument().getRequisitionSourceCode();

            if (StringUtils.isNotEmpty(reqSourceRequiringCatalogMatch)) {
                String[] requisitionSourcesFromParam = StringUtils.split(reqSourceRequiringCatalogMatch, ';');
                if (ArrayUtils.contains(requisitionSourcesFromParam, requisitionSourceCodeInPO)) {
                    String extraDescription = "Invoice Catalog No:" + invoiceCatalogNumberStripped + ", PO Catalog No:" + poItem.getItemCatalogNumber();
                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.CATALOG_NUMBER_MISMATCH, extraDescription, orderHolder.getFileName());
                    orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_CATALOG_NUMBER, PurapKeyConstants.ERROR_REJECT_CATALOG_MISMATCH);
                }
            }
        }
    }

    protected void validateQtyBasedItem(ElectronicInvoiceItemHolder itemHolder) {

        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();

        String fileName = itemHolder.getInvoiceOrderHolder().getFileName();
        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();

        if (KualiDecimal.ZERO.compareTo(poItem.getItemOutstandingEncumberedQuantity()) >= 0) {
            //we have no quantity left encumbered on the po item
            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.OUTSTANDING_ENCUMBERED_QTY_AVAILABLE, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_QUANTITY, PurapKeyConstants.ERROR_REJECT_POITEM_OUTSTANDING_QTY);
            return;
        }

        if (itemHolder.getInvoiceItemQuantity() == null) {
            //we have quantity entered on the PO Item but the Invoice has no quantity
            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_QTY_EMPTY, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_QUANTITY, PurapKeyConstants.ERROR_REJECT_POITEM_INVOICE_QTY_EMPTY);
            return;
        } else {

            if (!itemHolder.getInvoiceOrderHolder().getPurchaseOrderDocument().isReceivingDocumentRequiredIndicator()) {

                if ((itemHolder.getInvoiceItemQuantity().compareTo(poItem.getItemOutstandingEncumberedQuantity().bigDecimalValue())) > 0) {
                    //we have more quantity on the e-invoice than left outstanding encumbered on the PO item
                    String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ITEM_QTY_LESSTHAN_INVOICE_ITEM_QTY, extraDescription, orderHolder.getFileName());
                    orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_QUANTITY, PurapKeyConstants.ERROR_REJECT_POITEM_LESS_OUTSTANDING_QTY);
                    return;
                }
            }
        }

    }

    protected void validateNonQtyBasedItem(ElectronicInvoiceItemHolder itemHolder) {

        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();

        String fileName = itemHolder.getInvoiceOrderHolder().getFileName();
        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();

        if ((KualiDecimal.ZERO.compareTo(poItem.getItemOutstandingEncumberedAmount())) >= 0) {
            //we have no dollars left encumbered on the po item
            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.OUTSTANDING_ENCUMBERED_AMT_AVAILABLE, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_POITEM_OUTSTANDING_EMCUMBERED_AMOUNT);
            return;
        } else {
            //we have encumbered dollars left on PO
            if (((itemHolder.getInvoiceItemSubTotalAmount().setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR)).compareTo(poItem.getItemOutstandingEncumberedAmount().bigDecimalValue())) > 0) {
                String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ITEM_AMT_LESSTHAN_INVOICE_ITEM_AMT, extraDescription, orderHolder.getFileName());
                orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_POITEM_LESS_OUTSTANDING_EMCUMBERED_AMOUNT);
                return;
            }

        }
    }

    protected void validateUnitPrice(ElectronicInvoiceItemHolder itemHolder) {

        PurchaseOrderCostSource costSource = itemHolder.getInvoiceOrderHolder().getPurchaseOrderDocument().getPurchaseOrderCostSource();
        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();

        String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();

        BigDecimal actualVariance = itemHolder.getInvoiceItemUnitPrice().subtract(poItem.getItemUnitPrice());

        BigDecimal lowerPercentage = null;
        if (costSource.getItemUnitPriceLowerVariancePercent() != null) {
            //Checking for lower variance
            lowerPercentage = costSource.getItemUnitPriceLowerVariancePercent();
        } else {
            //If the cost source itemUnitPriceLowerVariancePercent is null then
            //we'll use the exact match (100%).
            lowerPercentage = new BigDecimal(100);
        }

        BigDecimal lowerAcceptableVariance = (lowerPercentage.divide(new BigDecimal(100))).multiply(poItem.getItemUnitPrice()).negate();

        if (lowerAcceptableVariance.compareTo(actualVariance) > 0) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_AMT_LESSER_THAN_LOWER_VARIANCE, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_UNIT_PRICE, PurapKeyConstants.ERROR_REJECT_UNITPRICE_LOWERVARIANCE);
        }

        BigDecimal upperPercentage = null;

        if (costSource.getItemUnitPriceUpperVariancePercent() != null) {
            //Checking for upper variance
            upperPercentage = costSource.getItemUnitPriceUpperVariancePercent();
        } else {
            //If the cost source itemUnitPriceLowerVariancePercent is null then
            //we'll use the exact match (100%).
            upperPercentage = new BigDecimal(100);
        }
        BigDecimal upperAcceptableVariance = (upperPercentage.divide(new BigDecimal(100))).multiply(poItem.getItemUnitPrice());

        if (upperAcceptableVariance.compareTo(actualVariance) < 0) {
            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_AMT_GREATER_THAN_UPPER_VARIANCE, extraDescription, orderHolder.getFileName());
            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_UNIT_PRICE, PurapKeyConstants.ERROR_REJECT_UNITPRICE_UPPERVARIANCE);
        }

    }

    protected void validateSalesTax(ElectronicInvoiceItemHolder itemHolder) {

        if (LOG.isInfoEnabled()) {
            LOG.info("Validating sales tax");
        }

        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();
        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
        KualiDecimal invoiceSalesTaxAmount = new KualiDecimal(itemHolder.getTaxAmount());

        boolean enableSalesTaxInd = SpringContext.getBean(ParameterService.class).getParameterValueAsBoolean(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_SALES_TAX_IND);

        boolean salesTaxUsed = false;
        PurchaseOrderDocument poDoc = orderHolder.getPurchaseOrderDocument();
        List<PurApItem> items = PurApItemUtils.getAboveTheLineOnly(poDoc.getItems());
        for (PurApItem item : items) {
            if (item.getItemType().isTaxableIndicator()) {
                salesTaxUsed = true;
                break;
            }
        }
        boolean useTaxUsed = poDoc.isUseTaxIndicator();
        enableSalesTaxInd &= (poItem.getItemType().isTaxableIndicator() && (salesTaxUsed || useTaxUsed));

        if (LOG.isInfoEnabled()) {
            LOG.info("Sales Tax Enable Indicator - " + enableSalesTaxInd);
            LOG.info("Invoice item tax amount - " + invoiceSalesTaxAmount);
        }
        if (!enableSalesTaxInd) {
            // if sales tax is disabled, item tax amount shall be zero
            if (invoiceSalesTaxAmount.compareTo(KualiDecimal.ZERO) != 0) {
                String extraDescription = "Item Tax Amount:" + invoiceSalesTaxAmount;
                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.TAX_SUMMARY_AMT_EXISTS, extraDescription, orderHolder.getFileName());
                orderHolder.addInvoiceHeaderRejectReason(rejectReason);
            }
            return;
        }

        // For reject doc, trans date should be the einvoice processed date.
        java.sql.Date transTaxDate = itemHolder.getInvoiceOrderHolder().getInvoiceProcessedDate();
        String deliveryPostalCode = poItem.getPurchaseOrder().getDeliveryPostalCode();
        KualiDecimal extendedPrice = new KualiDecimal(getExtendedPrice(itemHolder).setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR));

        KualiDecimal salesTaxAmountCalculated = taxService.getTotalSalesTaxAmount(transTaxDate, deliveryPostalCode, extendedPrice);
        KualiDecimal actualVariance = invoiceSalesTaxAmount.subtract(salesTaxAmountCalculated);

        if (LOG.isInfoEnabled()) {
            LOG.info("Sales Tax Upper Variance param - " + upperVariancePercentString);
            LOG.info("Sales Tax Lower Variance param - " + lowerVariancePercentString);
            LOG.info("Trans date (from invoice/rejectdoc) - " + transTaxDate);
            LOG.info("Delivery Postal Code - " + deliveryPostalCode);
            LOG.info("Extended price - " + extendedPrice);
            LOG.info("Sales Tax amount (from sales tax service) - " + salesTaxAmountCalculated);
        }

        if (StringUtils.isNotEmpty(upperVariancePercentString)) {

            KualiDecimal upperVariancePercent = new KualiDecimal(upperVariancePercentString);
            BigDecimal upperAcceptableVariance = (upperVariancePercent.divide(new KualiDecimal(100))).multiply(salesTaxAmountCalculated).bigDecimalValue();

            if (upperAcceptableVariance.compareTo(actualVariance.bigDecimalValue()) < 0) {
                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.SALES_TAX_AMT_GREATER_THAN_UPPER_VARIANCE, null, orderHolder.getFileName());
                orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_TAX_AMT, PurapKeyConstants.ERROR_REJECT_TAXAMOUNT_UPPERVARIANCE);
                return;
            }

        }

        if (StringUtils.isNotEmpty(lowerVariancePercentString)) {

            KualiDecimal lowerVariancePercent = new KualiDecimal(lowerVariancePercentString);
            BigDecimal lowerAcceptableVariance = (lowerVariancePercent.divide(new KualiDecimal(100))).multiply(salesTaxAmountCalculated).bigDecimalValue().negate();

            if (lowerAcceptableVariance.compareTo(BigDecimal.ZERO) >= 0 &&
                actualVariance.compareTo(KualiDecimal.ZERO) >= 0) {
                if (actualVariance.bigDecimalValue().compareTo(lowerAcceptableVariance) > 0) {
                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.SALES_TAX_AMT_LESSER_THAN_LOWER_VARIANCE, null, orderHolder.getFileName());
                    orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_TAX_AMT, PurapKeyConstants.ERROR_REJECT_TAXAMOUNT_LOWERVARIANCE);
                }
            } else {
                if (actualVariance.bigDecimalValue().compareTo(lowerAcceptableVariance) < 0) {
                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.SALES_TAX_AMT_LESSER_THAN_LOWER_VARIANCE, null, orderHolder.getFileName());
                    orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_TAX_AMT, PurapKeyConstants.ERROR_REJECT_TAXAMOUNT_LOWERVARIANCE);
                }
            }
        }

    }


    //Copied from PurApItemBase.calculateExtendedPrice
    protected BigDecimal getExtendedPrice(ElectronicInvoiceItemHolder itemHolder) {
        if (itemHolder.getPurchaseOrderItem().getItemType().isAmountBasedGeneralLedgerIndicator()) {
            // SERVICE ITEM: return unit price as extended price
            return itemHolder.getUnitPrice();
        } else if (ObjectUtils.isNotNull(itemHolder.getQuantity())) { // qty wont be null since it's defined as a reqd field in xsd
            BigDecimal calcExtendedPrice = itemHolder.getUnitPrice().multiply(itemHolder.getQuantity());
            // ITEM TYPE (qty driven): return (unitPrice x qty)
            return calcExtendedPrice;
        }
        return BigDecimal.ZERO;
    }

    @Override
    public ElectronicInvoiceRejectReason createRejectReason(String rejectReasonTypeCode, String extraDescription, String fileName) {

        ElectronicInvoiceRejectReasonType rejectReasonType = getElectronicInvoiceRejectReasonType(rejectReasonTypeCode);
        ElectronicInvoiceRejectReason eInvoiceRejectReason = new ElectronicInvoiceRejectReason();

        if (rejectReasonType == null) {
            throw new NullPointerException("Reject reason type for " + rejectReasonTypeCode + " not available in DB");
        }
        eInvoiceRejectReason.setInvoiceFileName(fileName);
        eInvoiceRejectReason.setInvoiceRejectReasonTypeCode(rejectReasonTypeCode);

        if (StringUtils.isNotEmpty(extraDescription)) {
            eInvoiceRejectReason.setInvoiceRejectReasonDescription(rejectReasonType.getInvoiceRejectReasonTypeDescription() + " (" + extraDescription + ")");
        } else {
            eInvoiceRejectReason.setInvoiceRejectReasonDescription(rejectReasonType.getInvoiceRejectReasonTypeDescription());
        }

        return eInvoiceRejectReason;

    }

    @Override
    public ElectronicInvoiceRejectReasonType getElectronicInvoiceRejectReasonType(String rejectReasonTypeCode) {
        if (rejectReasonTypes == null) {
            rejectReasonTypes = getElectronicInvoiceRejectReasonTypes();
        }
        return rejectReasonTypes.get(rejectReasonTypeCode);
    }

    protected Map<String, ElectronicInvoiceRejectReasonType> getElectronicInvoiceRejectReasonTypes() {

        Collection<ElectronicInvoiceRejectReasonType> collection = SpringContext.getBean(BusinessObjectService.class).findAll(ElectronicInvoiceRejectReasonType.class);
        Map rejectReasonTypesMap = new HashMap<String, ElectronicInvoiceRejectReasonType>();

        if (collection != null &&
            collection.size() > 0) {
            ElectronicInvoiceRejectReasonType[] rejectReasonTypesArr = new ElectronicInvoiceRejectReasonType[collection.size()];
            collection.toArray(rejectReasonTypesArr);
            for (int i = 0; i < rejectReasonTypesArr.length; i++) {
                rejectReasonTypesMap.put(rejectReasonTypesArr[i].getInvoiceRejectReasonTypeCode(), rejectReasonTypesArr[i]);
            }
        }

        return rejectReasonTypesMap;
    }

    public void setVendorService(VendorService vendorService) {
        this.vendorService = vendorService;
    }

    public void setTaxService(TaxService taxService) {
        this.taxService = taxService;
    }

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

}
