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.ar.document.validation.impl;
017    
018    import java.util.HashMap;
019    import java.util.List;
020    import java.util.Map;
021    
022    import org.apache.commons.lang.StringUtils;
023    import org.kuali.kfs.gl.service.EntryService;
024    import org.kuali.kfs.module.ar.ArAuthorizationConstants;
025    import org.kuali.kfs.module.ar.ArConstants;
026    import org.kuali.kfs.module.ar.ArKeyConstants;
027    import org.kuali.kfs.module.ar.ArPropertyConstants;
028    import org.kuali.kfs.module.ar.businessobject.CashControlDetail;
029    import org.kuali.kfs.module.ar.businessobject.Customer;
030    import org.kuali.kfs.module.ar.businessobject.PaymentMedium;
031    import org.kuali.kfs.module.ar.document.CashControlDocument;
032    import org.kuali.kfs.module.ar.document.PaymentApplicationDocument;
033    import org.kuali.kfs.module.ar.document.authorization.CashControlDocumentPresentationController;
034    import org.kuali.kfs.module.ar.document.validation.AddCashControlDetailRule;
035    import org.kuali.kfs.module.ar.document.validation.DeleteCashControlDetailRule;
036    import org.kuali.kfs.module.ar.document.validation.GenerateReferenceDocumentRule;
037    import org.kuali.kfs.sys.KFSConstants;
038    import org.kuali.kfs.sys.KFSPropertyConstants;
039    import org.kuali.kfs.sys.businessobject.Bank;
040    import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
041    import org.kuali.kfs.sys.context.SpringContext;
042    import org.kuali.kfs.sys.service.BankService;
043    import org.kuali.rice.kns.document.Document;
044    import org.kuali.rice.kns.rule.event.ApproveDocumentEvent;
045    import org.kuali.rice.kns.rules.TransactionalDocumentRuleBase;
046    import org.kuali.rice.kns.service.BusinessObjectService;
047    import org.kuali.rice.kns.service.DictionaryValidationService;
048    import org.kuali.rice.kns.service.DocumentHelperService;
049    import org.kuali.rice.kns.service.DocumentService;
050    import org.kuali.rice.kns.util.GlobalVariables;
051    import org.kuali.rice.kns.util.MessageMap;
052    import org.kuali.rice.kns.util.ObjectUtils;
053    import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument;
054    
055    /**
056     * This class holds the business rules for the AR Cash Control Document
057     */
058    public class CashControlDocumentRule extends TransactionalDocumentRuleBase implements AddCashControlDetailRule<CashControlDocument>, DeleteCashControlDetailRule<CashControlDocument>, GenerateReferenceDocumentRule<CashControlDocument> {
059    
060        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CashControlDocumentRule.class);
061    
062        /**
063         * @see org.kuali.rice.kns.rules.TransactionalDocumentRuleBase#processCustomSaveDocumentBusinessRules(Document)
064         */
065        @Override
066        protected boolean processCustomSaveDocumentBusinessRules(Document document) {
067    
068            boolean isValid = super.processCustomSaveDocumentBusinessRules(document);
069            CashControlDocument ccDocument = (CashControlDocument) document;
070    
071            ccDocument.refreshReferenceObject("customerPaymentMedium");
072            ccDocument.refreshReferenceObject("generalLedgerPendingEntries");
073    
074            MessageMap errorMap = GlobalVariables.getMessageMap();
075    
076            if (errorMap.isEmpty()) {
077                isValid &= checkRefDocNumber(ccDocument);
078                isValid &= validateCashControlDetails(ccDocument);
079            }
080    
081            return isValid;
082    
083        }
084    
085        /**
086         * @see org.kuali.rice.kns.rules.TransactionalDocumentRuleBase#processCustomRouteDocumentBusinessRules(Document)
087         */
088        @Override
089        protected boolean processCustomRouteDocumentBusinessRules(Document document) {
090    
091            boolean isValid = super.processCustomRouteDocumentBusinessRules(document);
092            CashControlDocument cashControlDocument = (CashControlDocument) document;
093    
094            if (isValid) {
095                isValid &= checkPaymentMedium(cashControlDocument);
096                isValid &= checkRefDocNumber(cashControlDocument);
097                isValid &= validateBankCode(cashControlDocument);
098                isValid &= validateCashControlDetails(cashControlDocument);
099                isValid &= checkCashControlDocumentHasDetails(cashControlDocument);
100            }
101    
102            return isValid;
103    
104        }
105    
106        /**
107         * @see org.kuali.rice.kns.rules.TransactionalDocumentRuleBase#processCustomApproveDocumentBusinessRules(Document)
108         */
109        @Override
110        protected boolean processCustomApproveDocumentBusinessRules(ApproveDocumentEvent approveEvent) {
111    
112            boolean isValid = super.processCustomApproveDocumentBusinessRules(approveEvent);
113            CashControlDocument cashControlDocument = (CashControlDocument) approveEvent.getDocument();
114    
115            cashControlDocument.refreshReferenceObject("customerPaymentMedium");
116            cashControlDocument.refreshReferenceObject("generalLedgerPendingEntries");
117    
118            isValid &= checkAllAppDocsApproved(cashControlDocument);
119            isValid &= checkGLPEsCreated(cashControlDocument);
120    
121            return isValid;
122    
123        }
124    
125        /**
126         * This method checks the CashControlDetail line amount is not zero or negative.
127         * 
128         * @param document the CashControldocument
129         * @param detail the CashControlDetail
130         * @return true is amount is valid, false otherwise
131         */
132        public boolean checkLineAmount(CashControlDocument document, CashControlDetail detail) {
133    
134            boolean isValid = true;
135    
136            // line amount cannot be zero
137            if (detail.getFinancialDocumentLineAmount().isZero()) {
138                GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.FINANCIAL_DOCUMENT_LINE_AMOUNT, ArKeyConstants.ERROR_LINE_AMOUNT_CANNOT_BE_ZERO);
139                isValid = false;
140            }
141            // line amount cannot be negative
142            if (detail.getFinancialDocumentLineAmount().isNegative()) {
143                GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.FINANCIAL_DOCUMENT_LINE_AMOUNT, ArKeyConstants.ERROR_LINE_AMOUNT_CANNOT_BE_NEGATIVE);
144                isValid = false;
145            }
146            return isValid;
147    
148        }
149    
150        /**
151         * This method checks if the CashControlDocument has any details to be processed.
152         * 
153         * @param cashControlDocument the CashControlDocument
154         * @return true if it has details, false otherwise
155         */
156        public boolean checkCashControlDocumentHasDetails(CashControlDocument cashControlDocument) {
157    
158            boolean isValid = true;
159            GlobalVariables.getMessageMap().addToErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
160    
161            if (cashControlDocument.getCashControlDetails().isEmpty()) {
162                GlobalVariables.getMessageMap().putError(KFSPropertyConstants.CASH_CONTROL_DETAILS, ArKeyConstants.ERROR_NO_LINES_TO_PROCESS);
163            }
164    
165            GlobalVariables.getMessageMap().removeFromErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
166    
167            return isValid;
168    
169        }
170    
171        /**
172         * This method checks that payment medium has a valid value
173         * 
174         * @param document
175         * @return true if valid, false otherwise
176         */
177        public boolean checkPaymentMedium(CashControlDocument document) {
178    
179            boolean isValid = true;
180            GlobalVariables.getMessageMap().addToErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
181            String paymentMediumCode = document.getCustomerPaymentMediumCode();
182    
183            Map<String, String> criteria = new HashMap<String, String>();
184            criteria.put("customerPaymentMediumCode", paymentMediumCode);
185    
186            PaymentMedium paymentMedium = (PaymentMedium) SpringContext.getBean(BusinessObjectService.class).findByPrimaryKey(PaymentMedium.class, criteria);
187    
188            if (paymentMedium == null) {
189                GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.CUSTOMER_PAYMENT_MEDIUM_CODE, ArKeyConstants.ERROR_PAYMENT_MEDIUM_IS_NOT_VALID);
190                isValid = false;
191            }
192    
193            GlobalVariables.getMessageMap().removeFromErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
194            return isValid;
195    
196        }
197    
198        /**
199         * This method checks that reference document number is not null when payment medium is Cash.
200         * 
201         * @param document CashControlDocument
202         * @return true if valid, false otherwise
203         */
204        public boolean checkRefDocNumber(CashControlDocument document) {
205    
206            boolean isValid = true;
207            GlobalVariables.getMessageMap().addToErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
208            String paymentMedium = document.getCustomerPaymentMediumCode();
209            if (ArConstants.PaymentMediumCode.CASH.equalsIgnoreCase(paymentMedium)) {
210                String refDocNumber = document.getReferenceFinancialDocumentNumber();
211                try {
212                    Long.parseLong(refDocNumber);
213                    if (StringUtils.isBlank(refDocNumber)) {
214                        GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.REFERENCE_FINANCIAL_DOC_NBR, ArKeyConstants.ERROR_REFERENCE_DOC_NUMBER_CANNOT_BE_NULL_FOR_PAYMENT_MEDIUM_CASH);
215                        isValid = false;
216                    }
217                    else {
218                        boolean docExists = SpringContext.getBean(DocumentService.class).documentExists(refDocNumber);
219                        if (!docExists) {
220                            GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.REFERENCE_FINANCIAL_DOC_NBR, ArKeyConstants.ERROR_REFERENCE_DOC_NUMBER_MUST_BE_VALID_FOR_PAYMENT_MEDIUM_CASH);
221                            isValid = false;
222                        }
223                    }
224                }
225                catch (NumberFormatException nfe) {
226                    GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.REFERENCE_FINANCIAL_DOC_NBR, ArKeyConstants.ERROR_REFERENCE_DOC_NUMBER_MUST_BE_VALID_FOR_PAYMENT_MEDIUM_CASH);
227                    isValid = false;
228                }
229    
230            }
231            GlobalVariables.getMessageMap().removeFromErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
232            return isValid;
233    
234        }
235    
236        /**
237         * This method checks that the GLPEs have been created
238         * 
239         * @param document CashControlDocument
240         * @return true if not null, false otherwise
241         */
242        public boolean checkGLPEsCreated(CashControlDocument cashControlDocument) {
243    
244            boolean isValid = true;
245            List<GeneralLedgerPendingEntry> glpes = cashControlDocument.getGeneralLedgerPendingEntries();
246            
247            Integer totalGLRecordsCreated = 0;
248            
249            if (glpes == null || glpes.isEmpty()) {
250                totalGLRecordsCreated = cashControlDocument.getGeneralLedgerEntriesPostedCount();
251            }
252            
253            // if payment medium is not Cash the general ledger pending entries must not be empty; if payment medium is Cash then a Cash
254            // Receipt Document must be created prior to creating the Cash Control document and it's number should be set in Reference
255            // Document number
256            if (!ArConstants.PaymentMediumCode.CASH.equalsIgnoreCase(cashControlDocument.getCustomerPaymentMediumCode()) && ((glpes == null || glpes.isEmpty()) && totalGLRecordsCreated.intValue() == 0)) {
257                GlobalVariables.getMessageMap().putError(KFSConstants.GENERAL_LEDGER_PENDING_ENTRIES_TAB_ERRORS, ArKeyConstants.ERROR_GLPES_NOT_CREATED);
258                isValid = false;
259            }
260    
261            return isValid;
262    
263        }
264    
265        /**
266         * @see org.kuali.kfs.module.ar.document.validation.AddCashControlDetailRule#processAddCashControlDetailBusinessRules(org.kuali.rice.kns.document.TransactionalDocument,
267         *      org.kuali.kfs.module.ar.businessobject.CashControlDetail)
268         */
269        public boolean processAddCashControlDetailBusinessRules(CashControlDocument transactionalDocument, CashControlDetail cashControlDetail) {
270    
271            boolean success = true;
272    
273            success &= checkGLPEsNotGenerated(transactionalDocument);
274            if (success) {
275                GlobalVariables.getMessageMap().removeFromErrorPath(ArConstants.NEW_CASH_CONTROL_DETAIL_ERROR_PATH_PREFIX);
276                success &= validateBankCode(transactionalDocument);
277                GlobalVariables.getMessageMap().addToErrorPath(ArConstants.NEW_CASH_CONTROL_DETAIL_ERROR_PATH_PREFIX);
278    
279                success &= validateCashControlDetail(transactionalDocument, cashControlDetail);
280            }
281            return success;
282    
283        }
284    
285        /**
286         * This method validates CashControlDetail
287         * 
288         * @param document CashControlDocument
289         * @param cashControlDetail CashControlDetail
290         * @return true if CashControlDetail is valid, false otherwise
291         */
292        protected boolean validateCashControlDetail(CashControlDocument document, CashControlDetail cashControlDetail) {
293    
294            MessageMap errorMap = GlobalVariables.getMessageMap();
295    
296            boolean isValid = true;
297    
298            int originalErrorCount = errorMap.getErrorCount();
299            // call the DD validation which checks basic data integrity
300            SpringContext.getBean(DictionaryValidationService.class).validateBusinessObject(cashControlDetail);
301            isValid = (errorMap.getErrorCount() == originalErrorCount);
302    
303            // validate customer number and line amount
304            if (isValid) {
305                String customerNumber = cashControlDetail.getCustomerNumber();
306                // if customer number is not empty check that it is valid
307                if (customerNumber != null && !customerNumber.equals("")) {
308                    isValid &= checkCustomerNumber(customerNumber);
309                }
310                // check if line amount is valid
311                isValid &= checkLineAmount(document, cashControlDetail);
312            }
313    
314            return isValid;
315    
316        }
317    
318        /**
319         * This method validates cash control document's details
320         * 
321         * @param cashControlDocument CashControldocument
322         * @return true if valid, false otherwise
323         */
324        public boolean validateCashControlDetails(CashControlDocument cashControlDocument) {
325    
326            GlobalVariables.getMessageMap().addToErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
327            boolean isValid = true;
328    
329            for (int i = 0; i < cashControlDocument.getCashControlDetails().size(); i++) {
330    
331                CashControlDetail cashControlDetail = cashControlDocument.getCashControlDetail(i);
332                String propertyName = KFSPropertyConstants.CASH_CONTROL_DETAIL + "[" + i + "]";
333                GlobalVariables.getMessageMap().addToErrorPath(propertyName);
334    
335                isValid &= validateCashControlDetail(cashControlDocument, cashControlDetail);
336    
337                GlobalVariables.getMessageMap().removeFromErrorPath(propertyName);
338            }
339    
340            GlobalVariables.getMessageMap().removeFromErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
341            return isValid;
342    
343        }
344    
345        /**
346         * This method checks that the customer number is valid and not an inactive customer when it is not empty
347         * 
348         * @param cashControlDetail
349         * @return true if valid, false otherwise
350         */
351        protected boolean checkCustomerNumber(String customerNumber) {
352            boolean isValid = true;
353    
354            if (customerNumber != null && !customerNumber.equals("")) {
355    
356                Map<String, String> criteria = new HashMap<String, String>();
357                criteria.put("customerNumber", customerNumber);
358    
359                Customer customer = (Customer) SpringContext.getBean(BusinessObjectService.class).findByPrimaryKey(Customer.class, criteria);
360    
361                if (customer == null) {
362                    GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.CUSTOMER_NUMBER, ArKeyConstants.ERROR_CUSTOMER_NUMBER_IS_NOT_VALID);
363                    isValid = false;
364                }
365            }
366    
367            return isValid;
368        }
369    
370        /**
371         * This method checks if GLPEs have been already generated
372         * 
373         * @param cashControlDocument the cash control document
374         * @return true if it was not generated, false otherwise
375         */
376        public boolean checkGLPEsNotGenerated(CashControlDocument cashControlDocument) {
377    
378            boolean success = true;
379            List<GeneralLedgerPendingEntry> glpes = cashControlDocument.getGeneralLedgerPendingEntries();
380    
381            if (glpes != null && !glpes.isEmpty()) {
382                success = false;
383                GlobalVariables.getMessageMap().putError(KFSConstants.GENERAL_LEDGER_PENDING_ENTRIES_TAB_ERRORS, ArKeyConstants.ERROR_DELETE_ADD_APP_DOCS_NOT_ALLOWED_AFTER_GLPES_GEN);
384            }
385            return success;
386    
387        }
388    
389        /**
390         * This method checks if all application documents are in approved or in final state
391         * 
392         * @param cashControlDocument
393         * @return true if all application documents approved/final, false otherwise
394         */
395        public boolean checkAllAppDocsApproved(CashControlDocument cashControlDocument) {
396    
397            boolean allAppDocsApproved = true;
398    
399            for (int i = 0; i < cashControlDocument.getCashControlDetails().size(); i++) {
400    
401                CashControlDetail cashControlDetail = cashControlDocument.getCashControlDetail(i);
402                PaymentApplicationDocument applicationDocument = cashControlDetail.getReferenceFinancialDocument();
403                KualiWorkflowDocument workflowDocument = applicationDocument.getDocumentHeader().getWorkflowDocument();
404    
405                if (!(workflowDocument.stateIsApproved() || workflowDocument.stateIsFinal())) {
406                    allAppDocsApproved = false;
407    
408                    String propertyName = KFSPropertyConstants.CASH_CONTROL_DETAIL + "[" + i + "]";
409                    GlobalVariables.getMessageMap().addToErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
410                    GlobalVariables.getMessageMap().addToErrorPath(propertyName);
411                    GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.APPLICATION_DOC_STATUS, ArKeyConstants.ERROR_ALL_APPLICATION_DOCS_MUST_BE_APPROVED);
412                    GlobalVariables.getMessageMap().removeFromErrorPath(propertyName);
413                    GlobalVariables.getMessageMap().removeFromErrorPath(KFSConstants.DOCUMENT_PROPERTY_NAME);
414    
415                    break;
416                }
417    
418            }
419    
420            return allAppDocsApproved;
421    
422        }
423    
424        /**
425         * @see org.kuali.kfs.module.ar.document.validation.DeleteCashControlDetailRule#processDeleteCashControlDetailBusinessRules(org.kuali.rice.kns.document.TransactionalDocument,
426         *      org.kuali.kfs.module.ar.businessobject.CashControlDetail)
427         */
428        public boolean processDeleteCashControlDetailBusinessRules(CashControlDocument transactionalDocument, CashControlDetail cashControlDetail) {
429    
430            boolean success = true;
431            success &= checkGLPEsNotGenerated(transactionalDocument);
432            return success;
433    
434        }
435    
436        /**
437         * @see org.kuali.kfs.module.ar.document.validation.GenerateReferenceDocumentRule#processGenerateReferenceDocumentBusinessRules(org.kuali.rice.kns.document.TransactionalDocument)
438         */
439        public boolean processGenerateReferenceDocumentBusinessRules(CashControlDocument transactionalDocument) {
440    
441            boolean success = true;
442            success &= checkPaymentMedium(transactionalDocument);
443            if (success) {
444                success &= checkGLPEsNotGenerated(transactionalDocument);
445            }
446            return success;
447    
448        }
449    
450        // validate bankCode
451        public boolean validateBankCode(CashControlDocument document) {
452            boolean isValid = true;
453    
454            // if the EDIT_BANK_CODE isnt enabled, then dont bother checking it, return with success
455            CashControlDocumentPresentationController ccPC = (CashControlDocumentPresentationController) SpringContext.getBean(DocumentHelperService.class).getDocumentPresentationController(document);
456            if (!ccPC.getEditModes(document).contains(ArAuthorizationConstants.CashControlDocumentEditMode.EDIT_BANK_CODE)) {
457                return isValid;
458            }
459    
460            // otherwise, make sure it exists and is valid
461            String bankCode = document.getBankCode();
462            if (StringUtils.isNotBlank(bankCode)) {
463                Bank bank = SpringContext.getBean(BankService.class).getByPrimaryId(bankCode);
464                if (ObjectUtils.isNull(bank)) {
465                    isValid = false;
466                    GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.BANK_CODE, ArKeyConstants.ERROR_INVALID_BANK_CODE);
467                }
468                else {
469                    // make sure the bank is eligible for deposit activity
470                    if (!bank.isBankDepositIndicator()) {
471                        isValid = false;
472                        GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.BANK_CODE, ArKeyConstants.ERROR_BANK_NOT_ELIGIBLE_FOR_DEPOSIT_ACTIVITY);
473                    }
474                }
475            }
476            else {
477                if (SpringContext.getBean(BankService.class).isBankSpecificationEnabled()) {
478                    isValid = false;
479                    GlobalVariables.getMessageMap().putError(ArPropertyConstants.CashControlDocumentFields.BANK_CODE, ArKeyConstants.ERROR_BANK_CODE_REQUIRED);
480                }
481            }
482    
483            return isValid;
484        }
485    
486    }