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