001/*
002 * Copyright 2008 The Kuali Foundation
003 * 
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 * http://www.opensource.org/licenses/ecl2.php
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.ole.module.purap.service.impl;
017
018import org.apache.commons.lang.ArrayUtils;
019import org.apache.commons.lang.StringUtils;
020import org.apache.commons.lang.math.NumberUtils;
021import org.apache.log4j.Logger;
022import org.kuali.ole.module.purap.PurapConstants;
023import org.kuali.ole.module.purap.PurapKeyConstants;
024import org.kuali.ole.module.purap.PurapParameterConstants;
025import org.kuali.ole.module.purap.batch.ElectronicInvoiceStep;
026import org.kuali.ole.module.purap.businessobject.*;
027import org.kuali.ole.module.purap.document.PurchaseOrderDocument;
028import org.kuali.ole.module.purap.service.ElectronicInvoiceMatchingService;
029import org.kuali.ole.module.purap.util.ElectronicInvoiceUtils;
030import org.kuali.ole.module.purap.util.PurApItemUtils;
031import org.kuali.ole.sys.context.SpringContext;
032import org.kuali.ole.sys.service.TaxService;
033import org.kuali.ole.sys.service.impl.OleParameterConstants;
034import org.kuali.ole.vnd.businessobject.PurchaseOrderCostSource;
035import org.kuali.ole.vnd.businessobject.VendorDetail;
036import org.kuali.ole.vnd.document.service.VendorService;
037import org.kuali.rice.core.api.datetime.DateTimeService;
038import org.kuali.rice.core.api.util.type.KualiDecimal;
039import org.kuali.rice.coreservice.framework.parameter.ParameterService;
040import org.kuali.rice.krad.service.BusinessObjectService;
041import org.kuali.rice.krad.util.ObjectUtils;
042
043import java.math.BigDecimal;
044import java.util.*;
045
046public class ElectronicInvoiceMatchingServiceImpl implements ElectronicInvoiceMatchingService {
047
048    private Logger LOG = Logger.getLogger(ElectronicInvoiceMatchingServiceImpl.class);
049
050    private Map<String, ElectronicInvoiceRejectReasonType> rejectReasonTypes;
051    private VendorService vendorService;
052    private TaxService taxService;
053    private DateTimeService dateTimeService;
054
055    String upperVariancePercentString;
056    String lowerVariancePercentString;
057
058    public void doMatchingProcess(ElectronicInvoiceOrderHolder orderHolder) {
059
060        if (LOG.isInfoEnabled()) {
061            LOG.info("Matching process started");
062        }
063
064        upperVariancePercentString = SpringContext.getBean(ParameterService.class).getParameterValueAsString(ElectronicInvoiceStep.class, PurapParameterConstants.ElectronicInvoiceParameters.SALES_TAX_UPPER_VARIANCE_PERCENT);
065        lowerVariancePercentString = SpringContext.getBean(ParameterService.class).getParameterValueAsString(ElectronicInvoiceStep.class, PurapParameterConstants.ElectronicInvoiceParameters.SALES_TAX_LOWER_VARIANCE_PERCENT);
066        ;
067
068        try {
069            if (orderHolder.isValidateHeaderInformation()) {
070
071                validateHeaderInformation(orderHolder);
072
073                if (orderHolder.isInvoiceRejected()) {
074                    if (LOG.isInfoEnabled()) {
075                        LOG.info("Matching process failed at header validation");
076                    }
077                    return;
078                }
079            }
080
081            validateInvoiceDetails(orderHolder);
082
083            if (orderHolder.isInvoiceRejected()) {
084                if (LOG.isInfoEnabled()) {
085                    LOG.info("Matching process failed at order detail validation");
086                }
087                return;
088            }
089
090        } catch (NumberFormatException e) {
091            if (LOG.isInfoEnabled()) {
092                LOG.info("Matching process matching failed due to number format exception " + e.getMessage());
093            }
094            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVALID_NUMBER_FORMAT, e.getMessage(), orderHolder.getFileName());
095            orderHolder.addInvoiceHeaderRejectReason(rejectReason);
096            return;
097        }
098
099        if (LOG.isInfoEnabled()) {
100            LOG.info("Matching process ended successfully");
101        }
102    }
103
104    protected void validateHeaderInformation(ElectronicInvoiceOrderHolder orderHolder) {
105
106        String dunsField = PurapConstants.ElectronicInvoice.RejectDocumentFields.VENDOR_DUNS_NUMBER;
107        String applnResourceKeyName = PurapKeyConstants.ERROR_REJECT_INVALID_DUNS;
108
109        if (StringUtils.isEmpty(orderHolder.getDunsNumber())) {
110            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUNS_NOT_FOUND, null, orderHolder.getFileName());
111            orderHolder.addInvoiceHeaderRejectReason(rejectReason, dunsField, applnResourceKeyName);
112            return;
113        }
114
115        if (orderHolder.isRejectDocumentHolder()) {
116            VendorDetail vendorDetail = SpringContext.getBean(VendorService.class).getVendorByDunsNumber(orderHolder.getDunsNumber());
117            if (vendorDetail == null) {
118                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUNS_INVALID, null, orderHolder.getFileName());
119                orderHolder.addInvoiceHeaderRejectReason(rejectReason, dunsField, applnResourceKeyName);
120                return;
121            }
122        } else {
123            if (orderHolder.getVendorHeaderId() == null && orderHolder.getVendorDetailId() == null) {
124                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUNS_INVALID, null, orderHolder.getFileName());
125                orderHolder.addInvoiceHeaderRejectReason(rejectReason, dunsField, applnResourceKeyName);
126                return;
127            }
128        }
129
130        String invoiceNumberField = PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_FILE_NUMBER;
131        if (!orderHolder.isInvoiceNumberAcceptIndicatorEnabled()) {
132            if (StringUtils.isEmpty(orderHolder.getInvoiceNumber())) {
133                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_ID_EMPTY, null, orderHolder.getFileName());
134                orderHolder.addInvoiceHeaderRejectReason(rejectReason, invoiceNumberField, PurapKeyConstants.ERROR_REJECT_INVOICE_NUMBER_EMPTY);
135                return;
136            }
137        }
138
139        String invoiceDateField = PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_FILE_DATE;
140
141        if (StringUtils.isEmpty(orderHolder.getInvoiceDateString()) || orderHolder.getInvoiceDate() == null) {
142            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_DATE_INVALID, null, orderHolder.getFileName());
143            orderHolder.addInvoiceHeaderRejectReason(rejectReason, invoiceDateField, PurapKeyConstants.ERROR_REJECT_INVOICE_DATE_INVALID);
144            return;
145        } else if (orderHolder.getInvoiceDate().after(dateTimeService.getCurrentDate())) {
146            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_DATE_GREATER, null, orderHolder.getFileName());
147            orderHolder.addInvoiceOrderRejectReason(rejectReason, invoiceDateField, PurapKeyConstants.ERROR_REJECT_INVOICE_DATE_GREATER);
148            return;
149        }
150
151        if (orderHolder.isInformationOnly()) {
152            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INFORMATION_ONLY, null, orderHolder.getFileName());
153            orderHolder.addInvoiceHeaderRejectReason(rejectReason);
154            return;
155        }
156
157        validateSummaryAmounts(orderHolder);
158
159        if (orderHolder.isInvoiceRejected()) {
160            return;
161        }
162
163        validateItemTypes(orderHolder);
164
165        if (orderHolder.isInvoiceRejected()) {
166            return;
167        }
168
169    }
170
171    protected void validateSummaryAmounts(ElectronicInvoiceOrderHolder orderHolder) {
172
173        if (orderHolder.isRejectDocumentHolder()) {
174            /**
175             * If there are any rejects related to the summary, we're retaining it since 
176             * it's not possible to get the summary amount totals from the reject doc
177             */
178            return;
179        }
180
181        ElectronicInvoiceDetailRequestSummary summary = orderHolder.getElectronicInvoice().getInvoiceDetailRequestSummary();
182
183        boolean enableSalesTaxInd = SpringContext.getBean(ParameterService.class).getParameterValueAsBoolean(OleParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_SALES_TAX_IND);
184
185        boolean salesTaxUsed = false;
186        PurchaseOrderDocument poDoc = orderHolder.getPurchaseOrderDocument();
187        if (poDoc != null) {  // we handle bad PO's in the eInvoice later, so just skip this
188            List<PurApItem> items = PurApItemUtils.getAboveTheLineOnly(poDoc.getItems());
189            for (PurApItem item : items) {
190                if (item.getItemType().isTaxableIndicator()) {
191                    salesTaxUsed = true;
192                    break;
193                }
194            }
195
196            boolean useTaxUsed = poDoc.isUseTaxIndicator();
197            enableSalesTaxInd &= (salesTaxUsed || useTaxUsed);
198
199            BigDecimal summaryTaxAmount = summary.getInvoiceTaxAmount();
200            if (!enableSalesTaxInd) {
201                // if sales tax is disabled, total tax amount shall be zero 
202                if (summaryTaxAmount.compareTo(new BigDecimal(0)) != 0) {
203                    String extraDescription = "Summary Tax Amount:" + summaryTaxAmount;
204                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.TAX_SUMMARY_AMT_EXISTS, extraDescription, orderHolder.getFileName());
205                    orderHolder.addInvoiceHeaderRejectReason(rejectReason);
206                }
207            } else if (orderHolder.isTaxInLine()) {
208                validateSummaryAmount(orderHolder, summaryTaxAmount, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_TAX, PurapConstants.ElectronicInvoice.TAX_SUMMARY_AMT_MISMATCH);
209            }
210        }
211
212        if (orderHolder.isShippingInLine()) {
213            validateSummaryAmount(orderHolder, summary.getInvoiceShippingAmount(), ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SHIPPING, PurapConstants.ElectronicInvoice.SHIPPING_SUMMARY_AMT_MISMATCH);
214        }
215
216        if (orderHolder.isSpecialHandlingInLine()) {
217            validateSummaryAmount(orderHolder, summary.getInvoiceSpecialHandlingAmount(), ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SPECIAL_HANDLING, PurapConstants.ElectronicInvoice.SPL_HANDLING_SUMMARY_AMT_MISMATCH);
218        }
219
220        if (orderHolder.isDiscountInLine()) {
221            validateSummaryAmount(orderHolder, summary.getInvoiceDiscountAmount(), ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_DISCOUNT, PurapConstants.ElectronicInvoice.DISCOUNT_SUMMARY_AMT_MISMATCH);
222        }
223
224    }
225
226    protected void validateSummaryAmount(ElectronicInvoiceOrderHolder orderHolder,
227                                         BigDecimal summaryAmount,
228                                         String invoiceLineItemTypeCode,
229                                         String rejectDescriptionCode) {
230
231        BigDecimal lineItemTotalAmount = orderHolder.getElectronicInvoice().getFileTotalAmountForInLineItems(invoiceLineItemTypeCode);
232
233//        if (lineItemTotalAmount.compareTo(BigDecimal.ZERO) != 0) { // old way, but it's not needed
234        if ((lineItemTotalAmount.compareTo(summaryAmount)) != 0) {
235            String extraDescription = "Line Total Amount:" + lineItemTotalAmount + ",Summary Total Amount:" + summaryAmount;
236            ElectronicInvoiceRejectReason rejectReason = createRejectReason(rejectDescriptionCode, extraDescription, orderHolder.getFileName());
237            orderHolder.addInvoiceHeaderRejectReason(rejectReason);
238        }
239//        }
240    }
241
242    protected void validateItemTypes(ElectronicInvoiceOrderHolder orderHolder) {
243
244        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_ITEM);
245        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_TAX);
246        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SHIPPING);
247        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SPECIAL_HANDLING);
248        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_DISCOUNT);
249        validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_EXMT);
250
251    }
252
253    protected void validateItemMapping(ElectronicInvoiceOrderHolder orderHolder, String kualiItemTypeCode) {
254
255        if (!orderHolder.isItemTypeAvailableInItemMapping(kualiItemTypeCode)) {
256            String extraDescription = kualiItemTypeCode;
257            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.ITEM_MAPPING_NOT_AVAILABLE, extraDescription, orderHolder.getFileName());
258            orderHolder.addInvoiceHeaderRejectReason(rejectReason);
259            return;
260        }
261
262    }
263
264    protected void validateInvoiceDetails(ElectronicInvoiceOrderHolder orderHolder) {
265
266        validatePurchaseOrderMatch(orderHolder);
267
268        if (orderHolder.isInvoiceRejected()) {
269            return;
270        }
271
272        validateInvoiceItems(orderHolder);
273
274        if (LOG.isInfoEnabled()) {
275            if (!orderHolder.isInvoiceRejected()) {
276                LOG.info("Purchase order document match done successfully");
277            }
278        }
279    }
280
281    protected void validatePurchaseOrderMatch(ElectronicInvoiceOrderHolder orderHolder) {
282
283        String poIDFieldName = PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_PO_ID;
284        String poID = orderHolder.getInvoicePurchaseOrderID();
285
286        if (StringUtils.isEmpty(poID)) {
287            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ID_EMPTY, null, orderHolder.getFileName());
288            orderHolder.addInvoiceOrderRejectReason(rejectReason, poIDFieldName, PurapKeyConstants.ERROR_REJECT_INVOICE_POID_EMPTY);
289            return;
290        }
291
292        String extraDesc = "Invoice Order ID:" + poID;
293
294        if (!NumberUtils.isDigits(poID)) {
295            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ID_INVALID_FORMAT, extraDesc, orderHolder.getFileName());
296            orderHolder.addInvoiceOrderRejectReason(rejectReason, poIDFieldName, PurapKeyConstants.ERROR_REJECT_INVOICE_POID_INVALID);
297            return;
298        }
299
300        PurchaseOrderDocument poDoc = orderHolder.getPurchaseOrderDocument();
301
302        if (poDoc == null) {
303            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_NOT_EXISTS, extraDesc, orderHolder.getFileName());
304            orderHolder.addInvoiceOrderRejectReason(rejectReason, poIDFieldName, PurapKeyConstants.ERROR_REJECT_INVOICE__PO_NOT_EXISTS);
305            return;
306        }
307
308        if (poDoc.getVendorHeaderGeneratedIdentifier() == null ||
309                poDoc.getVendorDetailAssignedIdentifier() == null ||
310                !(poDoc.getVendorHeaderGeneratedIdentifier().equals(orderHolder.getVendorHeaderId()) &&
311                        poDoc.getVendorDetailAssignedIdentifier().equals(orderHolder.getVendorDetailId()))) {
312            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_VENDOR_NOT_MATCHES_WITH_INVOICE_VENDOR, null, orderHolder.getFileName());
313            orderHolder.addInvoiceOrderRejectReason(rejectReason);
314            return;
315        }
316
317    }
318
319    protected void validateInvoiceItems(ElectronicInvoiceOrderHolder orderHolder) {
320
321        Set poLineNumbers = new HashSet();
322
323        ElectronicInvoiceItemHolder[] itemHolders = orderHolder.getItems();
324        if (itemHolders != null) {
325            for (int i = 0; i < itemHolders.length; i++) {
326                validateInvoiceItem(itemHolders[i], poLineNumbers);
327            }
328        }
329    }
330
331    protected void validateInvoiceItem(ElectronicInvoiceItemHolder itemHolder,
332                                       Set poLineNumbers) {
333
334        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
335        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();
336
337        if (poItem == null) {
338            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
339            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.NO_MATCHING_PO_ITEM, extraDescription, orderHolder.getFileName());
340            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_INVOICE__ITEM_NOMATCH);
341            return;
342        }
343
344        if (poLineNumbers.contains(itemHolder.getInvoiceItemLineNumber())) {
345            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
346            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUPLIATE_INVOICE_LINE_ITEM, extraDescription, orderHolder.getFileName());
347            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_PO_ITEM_DUPLICATE);
348            return;
349        } else {
350            poLineNumbers.add(itemHolder.getInvoiceItemLineNumber());
351        }
352
353        if (!poItem.isItemActiveIndicator()) {
354            String extraDescription = "PO Item Line Number:" + poItem.getItemLineNumber();
355            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INACTIVE_LINE_ITEM, extraDescription, orderHolder.getFileName());
356            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_PO_ITEM_INACTIVE);
357            return;
358        }
359
360        if (!itemHolder.isCatalogNumberAcceptIndicatorEnabled()) {
361            validateCatalogNumber(itemHolder);
362            if (orderHolder.isInvoiceRejected()) {
363                return;
364            }
365        }
366
367        if (!itemHolder.isUnitOfMeasureAcceptIndicatorEnabled()) {
368            if (!StringUtils.equals(poItem.getItemUnitOfMeasureCode(), itemHolder.getInvoiceItemUnitOfMeasureCode())) {
369                String extraDescription = "Invoice UOM:" + itemHolder.getInvoiceItemUnitOfMeasureCode() + ", PO UOM:" + poItem.getItemUnitOfMeasureCode();
370                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.UNIT_OF_MEASURE_MISMATCH, extraDescription, orderHolder.getFileName());
371                orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_UOM, PurapKeyConstants.ERROR_REJECT_UOM_MISMATCH);
372                return;
373            }
374        }
375
376        validateUnitPrice(itemHolder);
377
378        if (orderHolder.isInvoiceRejected()) {
379            return;
380        }
381
382        validateSalesTax(itemHolder);
383
384        if (orderHolder.isInvoiceRejected()) {
385            return;
386        }
387
388        if (poItem.getItemQuantity() != null) {
389            validateQtyBasedItem(itemHolder);
390        } else {
391            validateNonQtyBasedItem(itemHolder);
392        }
393
394    }
395
396    protected void validateCatalogNumber(ElectronicInvoiceItemHolder itemHolder) {
397
398        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
399        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();
400
401        String invoiceCatalogNumberStripped = itemHolder.getCatalogNumberStripped();
402        String poCatalogNumberStripped = ElectronicInvoiceUtils.stripSplChars(poItem.getItemCatalogNumber());
403
404        /**
405         * If Catalog number in invoice and po are not empty, create reject reason if it doesn't match 
406         */
407        if (StringUtils.isNotBlank(invoiceCatalogNumberStripped) &&
408                StringUtils.isNotBlank(poCatalogNumberStripped)) {
409
410            if (!StringUtils.equals(poCatalogNumberStripped, invoiceCatalogNumberStripped)) {
411
412                String extraDescription = "Invoice Catalog No:" + invoiceCatalogNumberStripped + ", PO Catalog No:" + poCatalogNumberStripped;
413                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.CATALOG_NUMBER_MISMATCH, extraDescription, orderHolder.getFileName());
414                orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_CATALOG_NUMBER, PurapKeyConstants.ERROR_REJECT_CATALOG_MISMATCH);
415            }
416
417        } else {
418
419            /**
420             * If catalog number is empty in PO/&Invoice, check whether the catalog check is required for the requisition source.
421             * If exists in param, create reject reason.
422             * If not exists, continue with UOM and unit price match.
423             */
424            String reqSourceRequiringCatalogMatch = SpringContext.getBean(ParameterService.class).getParameterValueAsString(ElectronicInvoiceStep.class, PurapParameterConstants.ElectronicInvoiceParameters.REQUISITION_SOURCES_REQUIRING_CATALOG_MATCHING);
425            String requisitionSourceCodeInPO = orderHolder.getPurchaseOrderDocument().getRequisitionSourceCode();
426
427            if (StringUtils.isNotEmpty(reqSourceRequiringCatalogMatch)) {
428                String[] requisitionSourcesFromParam = StringUtils.split(reqSourceRequiringCatalogMatch, ';');
429                if (ArrayUtils.contains(requisitionSourcesFromParam, requisitionSourceCodeInPO)) {
430                    String extraDescription = "Invoice Catalog No:" + invoiceCatalogNumberStripped + ", PO Catalog No:" + poItem.getItemCatalogNumber();
431                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.CATALOG_NUMBER_MISMATCH, extraDescription, orderHolder.getFileName());
432                    orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_CATALOG_NUMBER, PurapKeyConstants.ERROR_REJECT_CATALOG_MISMATCH);
433                }
434            }
435        }
436    }
437
438    protected void validateQtyBasedItem(ElectronicInvoiceItemHolder itemHolder) {
439
440        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
441
442        String fileName = itemHolder.getInvoiceOrderHolder().getFileName();
443        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();
444
445        if (KualiDecimal.ZERO.compareTo(poItem.getItemOutstandingEncumberedQuantity()) >= 0) {
446            //we have no quantity left encumbered on the po item
447            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
448            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.OUTSTANDING_ENCUMBERED_QTY_AVAILABLE, extraDescription, orderHolder.getFileName());
449            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_QUANTITY, PurapKeyConstants.ERROR_REJECT_POITEM_OUTSTANDING_QTY);
450            return;
451        }
452
453        if (itemHolder.getInvoiceItemQuantity() == null) {
454            //we have quantity entered on the PO Item but the Invoice has no quantity
455            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
456            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_QTY_EMPTY, extraDescription, orderHolder.getFileName());
457            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_QUANTITY, PurapKeyConstants.ERROR_REJECT_POITEM_INVOICE_QTY_EMPTY);
458            return;
459        } else {
460
461            if (!itemHolder.getInvoiceOrderHolder().getPurchaseOrderDocument().isReceivingDocumentRequiredIndicator()) {
462
463                if ((itemHolder.getInvoiceItemQuantity().compareTo(poItem.getItemOutstandingEncumberedQuantity().bigDecimalValue())) > 0) {
464                    //we have more quantity on the e-invoice than left outstanding encumbered on the PO item
465                    String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
466                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ITEM_QTY_LESSTHAN_INVOICE_ITEM_QTY, extraDescription, orderHolder.getFileName());
467                    orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_QUANTITY, PurapKeyConstants.ERROR_REJECT_POITEM_LESS_OUTSTANDING_QTY);
468                    return;
469                }
470            }
471        }
472
473    }
474
475    protected void validateNonQtyBasedItem(ElectronicInvoiceItemHolder itemHolder) {
476
477        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
478
479        String fileName = itemHolder.getInvoiceOrderHolder().getFileName();
480        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();
481
482        if ((KualiDecimal.ZERO.compareTo(poItem.getItemOutstandingEncumberedAmount())) >= 0) {
483            //we have no dollars left encumbered on the po item
484            String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
485            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.OUTSTANDING_ENCUMBERED_AMT_AVAILABLE, extraDescription, orderHolder.getFileName());
486            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_POITEM_OUTSTANDING_EMCUMBERED_AMOUNT);
487            return;
488        } else {
489            //we have encumbered dollars left on PO
490            if (((itemHolder.getInvoiceItemSubTotalAmount().setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR)).compareTo(poItem.getItemOutstandingEncumberedAmount().bigDecimalValue())) > 0) {
491                String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
492                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ITEM_AMT_LESSTHAN_INVOICE_ITEM_AMT, extraDescription, orderHolder.getFileName());
493                orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER, PurapKeyConstants.ERROR_REJECT_POITEM_LESS_OUTSTANDING_EMCUMBERED_AMOUNT);
494                return;
495            }
496
497        }
498    }
499
500    protected void validateUnitPrice(ElectronicInvoiceItemHolder itemHolder) {
501
502        PurchaseOrderCostSource costSource = itemHolder.getInvoiceOrderHolder().getPurchaseOrderDocument().getPurchaseOrderCostSource();
503        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
504        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();
505
506        String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber();
507
508        BigDecimal actualVariance = itemHolder.getInvoiceItemUnitPrice().subtract(poItem.getItemUnitPrice());
509
510        BigDecimal lowerPercentage = null;
511        if (costSource.getItemUnitPriceLowerVariancePercent() != null) {
512            //Checking for lower variance
513            lowerPercentage = costSource.getItemUnitPriceLowerVariancePercent();
514        } else {
515            //If the cost source itemUnitPriceLowerVariancePercent is null then
516            //we'll use the exact match (100%).
517            lowerPercentage = new BigDecimal(100);
518        }
519
520        BigDecimal lowerAcceptableVariance = (lowerPercentage.divide(new BigDecimal(100))).multiply(poItem.getItemUnitPrice()).negate();
521
522        if (lowerAcceptableVariance.compareTo(actualVariance) > 0) {
523            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_AMT_LESSER_THAN_LOWER_VARIANCE, extraDescription, orderHolder.getFileName());
524            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_UNIT_PRICE, PurapKeyConstants.ERROR_REJECT_UNITPRICE_LOWERVARIANCE);
525        }
526
527        BigDecimal upperPercentage = null;
528
529        if (costSource.getItemUnitPriceUpperVariancePercent() != null) {
530            //Checking for upper variance
531            upperPercentage = costSource.getItemUnitPriceUpperVariancePercent();
532        } else {
533            //If the cost source itemUnitPriceLowerVariancePercent is null then
534            //we'll use the exact match (100%).
535            upperPercentage = new BigDecimal(100);
536        }
537        BigDecimal upperAcceptableVariance = (upperPercentage.divide(new BigDecimal(100))).multiply(poItem.getItemUnitPrice());
538
539        if (upperAcceptableVariance.compareTo(actualVariance) < 0) {
540            ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_AMT_GREATER_THAN_UPPER_VARIANCE, extraDescription, orderHolder.getFileName());
541            orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_UNIT_PRICE, PurapKeyConstants.ERROR_REJECT_UNITPRICE_UPPERVARIANCE);
542        }
543
544    }
545
546    protected void validateSalesTax(ElectronicInvoiceItemHolder itemHolder) {
547
548        if (LOG.isInfoEnabled()) {
549            LOG.info("Validating sales tax");
550        }
551
552        ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder();
553        PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem();
554        KualiDecimal invoiceSalesTaxAmount = new KualiDecimal(itemHolder.getTaxAmount());
555
556        boolean enableSalesTaxInd = SpringContext.getBean(ParameterService.class).getParameterValueAsBoolean(OleParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_SALES_TAX_IND);
557
558        boolean salesTaxUsed = false;
559        PurchaseOrderDocument poDoc = orderHolder.getPurchaseOrderDocument();
560        List<PurApItem> items = PurApItemUtils.getAboveTheLineOnly(poDoc.getItems());
561        for (PurApItem item : items) {
562            if (item.getItemType().isTaxableIndicator()) {
563                salesTaxUsed = true;
564                break;
565            }
566        }
567        boolean useTaxUsed = poDoc.isUseTaxIndicator();
568        enableSalesTaxInd &= (poItem.getItemType().isTaxableIndicator() && (salesTaxUsed || useTaxUsed));
569
570        if (LOG.isInfoEnabled()) {
571            LOG.info("Sales Tax Enable Indicator - " + enableSalesTaxInd);
572            LOG.info("Invoice item tax amount - " + invoiceSalesTaxAmount);
573        }
574        if (!enableSalesTaxInd) {
575            // if sales tax is disabled, item tax amount shall be zero 
576            if (invoiceSalesTaxAmount.compareTo(KualiDecimal.ZERO) != 0) {
577                String extraDescription = "Item Tax Amount:" + invoiceSalesTaxAmount;
578                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.TAX_SUMMARY_AMT_EXISTS, extraDescription, orderHolder.getFileName());
579                orderHolder.addInvoiceHeaderRejectReason(rejectReason);
580            }
581            return;
582        }
583
584        // For reject doc, trans date should be the einvoice processed date.
585        java.sql.Date transTaxDate = itemHolder.getInvoiceOrderHolder().getInvoiceProcessedDate();
586        String deliveryPostalCode = poItem.getPurchaseOrder().getDeliveryPostalCode();
587        KualiDecimal extendedPrice = new KualiDecimal(getExtendedPrice(itemHolder).setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR));
588
589        KualiDecimal salesTaxAmountCalculated = taxService.getTotalSalesTaxAmount(transTaxDate, deliveryPostalCode, extendedPrice);
590        KualiDecimal actualVariance = invoiceSalesTaxAmount.subtract(salesTaxAmountCalculated);
591
592        if (LOG.isInfoEnabled()) {
593            LOG.info("Sales Tax Upper Variance param - " + upperVariancePercentString);
594            LOG.info("Sales Tax Lower Variance param - " + lowerVariancePercentString);
595            LOG.info("Trans date (from invoice/rejectdoc) - " + transTaxDate);
596            LOG.info("Delivery Postal Code - " + deliveryPostalCode);
597            LOG.info("Extended price - " + extendedPrice);
598            LOG.info("Sales Tax amount (from sales tax service) - " + salesTaxAmountCalculated);
599        }
600
601        if (StringUtils.isNotEmpty(upperVariancePercentString)) {
602
603            KualiDecimal upperVariancePercent = new KualiDecimal(upperVariancePercentString);
604            BigDecimal upperAcceptableVariance = (upperVariancePercent.divide(new KualiDecimal(100))).multiply(salesTaxAmountCalculated).bigDecimalValue();
605
606            if (upperAcceptableVariance.compareTo(actualVariance.bigDecimalValue()) < 0) {
607                ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.SALES_TAX_AMT_GREATER_THAN_UPPER_VARIANCE, null, orderHolder.getFileName());
608                orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_TAX_AMT, PurapKeyConstants.ERROR_REJECT_TAXAMOUNT_UPPERVARIANCE);
609                return;
610            }
611
612        }
613
614        if (StringUtils.isNotEmpty(lowerVariancePercentString)) {
615
616            KualiDecimal lowerVariancePercent = new KualiDecimal(lowerVariancePercentString);
617            BigDecimal lowerAcceptableVariance = (lowerVariancePercent.divide(new KualiDecimal(100))).multiply(salesTaxAmountCalculated).bigDecimalValue().negate();
618
619            if (lowerAcceptableVariance.compareTo(BigDecimal.ZERO) >= 0 &&
620                    actualVariance.compareTo(KualiDecimal.ZERO) >= 0) {
621                if (actualVariance.bigDecimalValue().compareTo(lowerAcceptableVariance) > 0) {
622                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.SALES_TAX_AMT_LESSER_THAN_LOWER_VARIANCE, null, orderHolder.getFileName());
623                    orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_TAX_AMT, PurapKeyConstants.ERROR_REJECT_TAXAMOUNT_LOWERVARIANCE);
624                }
625            } else {
626                if (actualVariance.bigDecimalValue().compareTo(lowerAcceptableVariance) < 0) {
627                    ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.SALES_TAX_AMT_LESSER_THAN_LOWER_VARIANCE, null, orderHolder.getFileName());
628                    orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_TAX_AMT, PurapKeyConstants.ERROR_REJECT_TAXAMOUNT_LOWERVARIANCE);
629                }
630            }
631        }
632
633    }
634
635
636    //Copied from PurApItemBase.calculateExtendedPrice
637    protected BigDecimal getExtendedPrice(ElectronicInvoiceItemHolder itemHolder) {
638        if (itemHolder.getPurchaseOrderItem().getItemType().isAmountBasedGeneralLedgerIndicator()) {
639            // SERVICE ITEM: return unit price as extended price
640            return itemHolder.getUnitPrice();
641        } else if (ObjectUtils.isNotNull(itemHolder.getQuantity())) { // qty wont be null since it's defined as a reqd field in xsd
642            BigDecimal calcExtendedPrice = itemHolder.getUnitPrice().multiply(itemHolder.getQuantity());
643            // ITEM TYPE (qty driven): return (unitPrice x qty)
644            return calcExtendedPrice;
645        }
646        return BigDecimal.ZERO;
647    }
648
649    public ElectronicInvoiceRejectReason createRejectReason(String rejectReasonTypeCode, String extraDescription, String fileName) {
650
651        ElectronicInvoiceRejectReasonType rejectReasonType = getElectronicInvoiceRejectReasonType(rejectReasonTypeCode);
652        ElectronicInvoiceRejectReason eInvoiceRejectReason = new ElectronicInvoiceRejectReason();
653
654        if (rejectReasonType == null) {
655            throw new NullPointerException("Reject reason type for " + rejectReasonTypeCode + " not available in DB");
656        }
657        eInvoiceRejectReason.setInvoiceFileName(fileName);
658        eInvoiceRejectReason.setInvoiceRejectReasonTypeCode(rejectReasonTypeCode);
659
660        if (StringUtils.isNotEmpty(extraDescription)) {
661            eInvoiceRejectReason.setInvoiceRejectReasonDescription(rejectReasonType.getInvoiceRejectReasonTypeDescription() + " (" + extraDescription + ")");
662        } else {
663            eInvoiceRejectReason.setInvoiceRejectReasonDescription(rejectReasonType.getInvoiceRejectReasonTypeDescription());
664        }
665
666        return eInvoiceRejectReason;
667
668    }
669
670    public ElectronicInvoiceRejectReasonType getElectronicInvoiceRejectReasonType(String rejectReasonTypeCode) {
671        if (rejectReasonTypes == null) {
672            rejectReasonTypes = getElectronicInvoiceRejectReasonTypes();
673        }
674        return rejectReasonTypes.get(rejectReasonTypeCode);
675    }
676
677    protected Map<String, ElectronicInvoiceRejectReasonType> getElectronicInvoiceRejectReasonTypes() {
678
679        Collection<ElectronicInvoiceRejectReasonType> collection = SpringContext.getBean(BusinessObjectService.class).findAll(ElectronicInvoiceRejectReasonType.class);
680        Map rejectReasonTypesMap = new HashMap<String, ElectronicInvoiceRejectReasonType>();
681
682        if (collection != null &&
683                collection.size() > 0) {
684            ElectronicInvoiceRejectReasonType[] rejectReasonTypesArr = new ElectronicInvoiceRejectReasonType[collection.size()];
685            collection.toArray(rejectReasonTypesArr);
686            for (int i = 0; i < rejectReasonTypesArr.length; i++) {
687                rejectReasonTypesMap.put(rejectReasonTypesArr[i].getInvoiceRejectReasonTypeCode(), rejectReasonTypesArr[i]);
688            }
689        }
690
691        return rejectReasonTypesMap;
692    }
693
694    public void setVendorService(VendorService vendorService) {
695        this.vendorService = vendorService;
696    }
697
698    public void setTaxService(TaxService taxService) {
699        this.taxService = taxService;
700    }
701
702    public void setDateTimeService(DateTimeService dateTimeService) {
703        this.dateTimeService = dateTimeService;
704    }
705
706}