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.text.MessageFormat;
019    import java.util.HashMap;
020    import java.util.List;
021    import java.util.Map;
022    
023    import org.apache.commons.lang.StringUtils;
024    import org.kuali.kfs.coa.businessobject.Account;
025    import org.kuali.kfs.coa.businessobject.Chart;
026    import org.kuali.kfs.coa.businessobject.ObjectCode;
027    import org.kuali.kfs.coa.businessobject.ProjectCode;
028    import org.kuali.kfs.coa.businessobject.SubAccount;
029    import org.kuali.kfs.coa.businessobject.SubObjectCode;
030    import org.kuali.kfs.module.ar.ArKeyConstants;
031    import org.kuali.kfs.module.ar.ArPropertyConstants;
032    import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail;
033    import org.kuali.kfs.module.ar.businessobject.InvoicePaidApplied;
034    import org.kuali.kfs.module.ar.businessobject.NonAppliedHolding;
035    import org.kuali.kfs.module.ar.businessobject.NonInvoiced;
036    import org.kuali.kfs.module.ar.document.CashControlDocument;
037    import org.kuali.kfs.module.ar.document.PaymentApplicationDocument;
038    import org.kuali.kfs.sys.context.SpringContext;
039    import org.kuali.rice.kew.exception.WorkflowException;
040    import org.kuali.rice.kns.service.BusinessObjectService;
041    import org.kuali.rice.kns.service.DictionaryValidationService;
042    import org.kuali.rice.kns.util.GlobalVariables;
043    import org.kuali.rice.kns.util.KualiDecimal;
044    import org.kuali.rice.kns.util.MessageMap;
045    import org.kuali.rice.kns.util.ObjectUtils;
046    
047    public class PaymentApplicationDocumentRuleUtil {
048    
049        public static boolean validateAllAmounts(PaymentApplicationDocument applicationDocument, List<CustomerInvoiceDetail> invoiceDetails, NonInvoiced newNonInvoiced, KualiDecimal totalFromControl) throws WorkflowException {
050            boolean isValid = validateApplieds(invoiceDetails, applicationDocument, totalFromControl);
051            isValid &= validateNonAppliedHolding(applicationDocument, totalFromControl);
052            isValid &= validateNonInvoiced(newNonInvoiced, applicationDocument, totalFromControl);
053            return isValid;
054        }
055        
056        /**
057         * This method checks that an invoice paid applied is for a valid amount.
058         * 
059         * @param invoicePaidApplied
060         * @return
061         */
062        public static boolean validateInvoicePaidApplied(InvoicePaidApplied invoicePaidApplied, String fieldName, KualiDecimal totalFromControl) {
063            boolean isValid = true;
064            
065            invoicePaidApplied.refreshReferenceObject("invoiceDetail");
066            if(ObjectUtils.isNull(invoicePaidApplied) || ObjectUtils.isNull(invoicePaidApplied.getInvoiceDetail())) { return true; }
067            KualiDecimal amountOwed = invoicePaidApplied.getInvoiceDetail().getAmountOpen();
068            KualiDecimal amountPaid = invoicePaidApplied.getInvoiceItemAppliedAmount();
069            
070            if(ObjectUtils.isNull(amountOwed)) {
071                amountOwed = KualiDecimal.ZERO;
072            }
073            if(ObjectUtils.isNull(amountPaid)) {
074                amountPaid = KualiDecimal.ZERO;
075            }
076            
077            // Can't pay more than you owe.
078            if(!amountPaid.isLessEqual(amountOwed)) {
079                isValid = false;
080                GlobalVariables.getMessageMap().putError(
081                    fieldName, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_EXCEEDS_AMOUNT_OUTSTANDING);
082            }
083            
084            // Can't apply more than the amount received via the related CashControlDocument
085            if (amountPaid.isGreaterThan(totalFromControl)) {
086                isValid = false;
087                GlobalVariables.getMessageMap().putError(
088                    fieldName,ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_APPLY_MORE_THAN_CASH_CONTROL_TOTAL_AMOUNT);
089            }
090            
091            //  cant apply negative amounts
092            if (amountPaid.isNegative()) {
093                isValid = false;
094                GlobalVariables.getMessageMap().putError(
095                        fieldName,ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_POSTIIVE);
096            }
097            return isValid;
098        }
099        
100        /**
101         * The sum of invoice paid applied amounts cannot exceed the cash control total amount
102         * 
103         * @param paymentApplicationDocument
104         * @return
105         * @throws WorkflowException
106         */
107        public static boolean validateCumulativeSumOfInvoicePaidAppliedDoesntExceedCashControlTotal(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
108            KualiDecimal appliedTotal = new KualiDecimal(0);
109            for(InvoicePaidApplied invoicePaidApplied : paymentApplicationDocument.getInvoicePaidApplieds()) {
110                invoicePaidApplied.refreshReferenceObject("invoiceDetail");
111                appliedTotal = appliedTotal.add(invoicePaidApplied.getInvoiceItemAppliedAmount());
112            }
113            return paymentApplicationDocument.getTotalFromControl().isGreaterEqual(appliedTotal);
114        }
115    
116        /**
117         * The sum of invoice paid applied amounts cannot be less than zero.
118         * 
119         * @param paymentApplicationDocument
120         * @return
121         * @throws WorkflowException
122         */
123        public static boolean validateCumulativeSumOfInvoicePaidAppliedsIsGreaterThanOrEqualToZero(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
124            KualiDecimal appliedTotal = new KualiDecimal(0);
125            for(InvoicePaidApplied invoicePaidApplied : paymentApplicationDocument.getInvoicePaidApplieds()) {
126                invoicePaidApplied.refreshReferenceObject("invoiceDetail");
127                appliedTotal = appliedTotal.add(invoicePaidApplied.getInvoiceItemAppliedAmount());
128            }
129            return KualiDecimal.ZERO.isLessEqual(appliedTotal);
130        }
131        
132        /**
133         * The sum of non invoiceds must be less than or equal to the cash control total amount
134         * 
135         * @param paymentApplicationDocument
136         * @return
137         * @throws WorkflowException
138         */
139        public static boolean validateNonInvoicedAmountDoesntExceedCashControlTotal(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
140            return paymentApplicationDocument.getTotalFromControl().isGreaterEqual(paymentApplicationDocument.getSumOfNonInvoiceds());
141        }
142        
143        /**
144         * The unapplied amount can't be negative
145         * 
146         * @param paymentApplicationDocument
147         * @return
148         * @throws WorkflowException
149         */
150        public static boolean validateNonInvoicedAmountIsGreaterThanOrEqualToZero(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
151            return KualiDecimal.ZERO.isLessEqual(paymentApplicationDocument.getSumOfNonInvoiceds());
152        }
153    
154        /**
155         * The unapplied amount must be less than or equal to the cash control total amount
156         * 
157         * @param paymentApplicationDocument
158         * @return
159         * @throws WorkflowException
160         */
161        public static boolean validateUnappliedAmountDoesntExceedCashControlTotal(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
162            KualiDecimal a = paymentApplicationDocument.getNonAppliedHoldingAmount();
163            if(null == a) {
164                return true;
165            }
166            return paymentApplicationDocument.getTotalFromControl().isGreaterEqual(a);
167        }
168        
169        /**
170         * The unapplied amount can't be negative
171         * 
172         * @param paymentApplicationDocument
173         * @return
174         * @throws WorkflowException
175         */
176        public static boolean validateUnappliedAmountIsGreaterThanOrEqualToZero(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
177            if(null == paymentApplicationDocument.getNonAppliedHoldingAmount()) { return true; }
178            return KualiDecimal.ZERO.isLessEqual(paymentApplicationDocument.getNonAppliedHoldingAmount());
179        }
180    
181        /**
182         * Validate non-ar/non-invoice line items on a PaymentApplicationDocument.
183         * 
184         * @param nonInvoiced
185         * @return
186         */
187        public static boolean validateNonInvoiced(NonInvoiced nonInvoiced, PaymentApplicationDocument paymentApplicationDocument, KualiDecimal totalFromControl) throws WorkflowException {
188            MessageMap errorMap = GlobalVariables.getMessageMap();
189            int originalErrorCount = errorMap.getErrorCount();
190            
191            //  validate the NonInvoiced BO
192            String sNonInvoicedErrorPath = "nonInvoicedAddLine";
193            errorMap.addToErrorPath(sNonInvoicedErrorPath);
194            SpringContext.getBean(DictionaryValidationService.class).validateBusinessObject(nonInvoiced);
195            errorMap.removeFromErrorPath(sNonInvoicedErrorPath);
196    
197            if (errorMap.getErrorCount() != originalErrorCount) {
198                return false;
199            }
200            
201            boolean isValid = true;
202            
203            // Required fields, so always validate these.
204            isValid &= validateNonInvoicedLineItem("chartOfAccountsCode", nonInvoiced.getChartOfAccountsCode(), Chart.class, 
205                    ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_CHART,
206                    ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_CHART_INVALID);
207            isValid &= validateNonInvoicedLineItem("accountNumber", nonInvoiced.getAccountNumber(), Account.class, 
208                    ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_ACCOUNT,
209                    ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_ACCOUNT_INVALID);
210            isValid &= validateNonInvoicedLineItem("financialObjectCode", nonInvoiced.getFinancialObjectCode(), ObjectCode.class, 
211                    ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_OBJECT,
212                    ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_OBJECT_CODE_INVALID);
213    
214            // Optional fields, so only validate if a value was entered.
215            if(StringUtils.isNotBlank(nonInvoiced.getSubAccountNumber())) {
216                isValid &= validateNonInvoicedLineItem("subAccountNumber", nonInvoiced.getSubAccountNumber(), SubAccount.class, 
217                        ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_SUBACCOUNT,
218                        ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_SUB_ACCOUNT_INVALID);
219            }
220            if(StringUtils.isNotBlank(nonInvoiced.getFinancialSubObjectCode())) {
221                isValid &= validateNonInvoicedLineItem("financialSubObjectCode", nonInvoiced.getFinancialSubObjectCode(), SubObjectCode.class, 
222                        ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_SUBOBJECT,
223                        ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_SUB_OBJECT_CODE_INVALID);
224            }
225            if(StringUtils.isNotBlank(nonInvoiced.getProjectCode())) {
226                isValid &= validateNonInvoicedLineItem("code", nonInvoiced.getProjectCode(), ProjectCode.class, 
227                        ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_PROJECT,
228                        ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_PROJECT_CODE_INVALID);
229            } 
230            
231            isValid &= validateNonInvoicedLineAmount(nonInvoiced, paymentApplicationDocument, totalFromControl);
232            
233            return isValid;
234        }
235    
236        /**
237         * This method validates the provided non invoiced line value.
238         * 
239         * @param attributeName The name of the attribute as it is defined within its parent business object (ie. financialObjectCode in ObjectCode.java)
240         * @param value The value of the NonInvoiced line to be validated.
241         * @param boClass The class that the provided value represents (ie. accountNumber represents Account.class)
242         * @param errorPropertyName The Payment Application document property name to be used for applying errors when necessary.
243         * @param errorMessageKey The error key path to be used for applying errors when necessary.
244         * @return True if the value provided is valid and exists, false otherwise.
245         */
246        private static boolean validateNonInvoicedLineItem(String attributeName, String value, Class boClass, String errorPropertyName, String errorMessageKey) {
247            MessageMap errorMap = GlobalVariables.getMessageMap();
248            boolean isValid = true;
249            Map<String, String> criteria = new HashMap<String, String>();
250            criteria.put(attributeName, value);
251    
252            Object object = SpringContext.getBean(BusinessObjectService.class).findByPrimaryKey(boClass, criteria);
253            if(ObjectUtils.isNull(object)) {
254                errorMap.putError(errorPropertyName, errorMessageKey, value);
255                isValid &= false;
256            }
257            return isValid;
258        }
259    
260        /**
261         * This method...
262         * @param nonInvoiced
263         * @param paymentApplicationDocument
264         * @param totalFromControl
265         * @return
266         */
267        private static boolean validateNonInvoicedLineAmount(NonInvoiced nonInvoiced, PaymentApplicationDocument paymentApplicationDocument, KualiDecimal totalFromControl) {
268            MessageMap errorMap = GlobalVariables.getMessageMap();
269            KualiDecimal nonArLineAmount = nonInvoiced.getFinancialDocumentLineAmount();
270            // check that dollar amount is not zero before continuing
271            if(ObjectUtils.isNull(nonArLineAmount)) {
272                errorMap.putError(
273                    ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT,
274                    ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_AMOUNT_REQUIRED);
275                return false;
276            } else {
277                KualiDecimal cashControlBalanceToBeApplied = totalFromControl;
278                cashControlBalanceToBeApplied = cashControlBalanceToBeApplied.add(paymentApplicationDocument.getTotalFromControl());
279                cashControlBalanceToBeApplied.subtract(paymentApplicationDocument.getTotalApplied());
280                cashControlBalanceToBeApplied.subtract(paymentApplicationDocument.getNonAppliedHoldingAmount());
281                
282                if (nonArLineAmount.isZero()) {
283                    errorMap.putError(
284                        ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT,
285                        ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_CANNOT_BE_ZERO);
286                    return false;
287                }
288                else if (nonArLineAmount.isNegative()) {
289                    errorMap.putError(
290                        ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT,
291                        ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_POSTIIVE);
292                    return false;
293                }
294                //  check that we're not trying to apply more funds to the invoice than the invoice has balance (ie, over-applying)
295                else if (KualiDecimal.ZERO.isGreaterThan(cashControlBalanceToBeApplied.subtract(nonArLineAmount))) {
296                    errorMap.putError(
297                        ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT,
298                        ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_AMOUNT_EXCEEDS_BALANCE_TO_BE_APPLIED);
299                    return false;
300                }
301    
302            }
303            return true;
304        }
305        
306        /**
307         * This method determines whether or not the amount to be applied to an invoice is acceptable.
308         * 
309         * @param customerInvoiceDetails
310         * @return
311         */
312        public static boolean validateApplieds(List<CustomerInvoiceDetail> customerInvoiceDetails, PaymentApplicationDocument paymentAplicationDocument, KualiDecimal totalFromControl) throws WorkflowException {
313    
314            // Indicates whether the validation succeeded
315            boolean isValid = true;
316    
317            // Figure out the maximum we should be able to apply.
318            for (CustomerInvoiceDetail customerInvoiceDetail : customerInvoiceDetails) {
319                isValid &= validateAmountAppliedToCustomerInvoiceDetailByPaymentApplicationDocument(customerInvoiceDetail, paymentAplicationDocument, totalFromControl);
320            }
321    
322            return isValid;
323        }
324        
325        /**
326         * @param customerInvoiceDetail
327         * @param paymentApplicationDocument
328         * @return
329         */
330        public static boolean validateAmountAppliedToCustomerInvoiceDetailByPaymentApplicationDocument(CustomerInvoiceDetail customerInvoiceDetail, PaymentApplicationDocument paymentApplicationDocument, KualiDecimal totalFromControl) throws WorkflowException {
331            
332            boolean isValid = true;
333            
334            // This let's us highlight a specific invoice detail line
335            String propertyName = 
336                MessageFormat.format(ArPropertyConstants.PaymentApplicationDocumentFields.AMOUNT_TO_BE_APPLIED_LINE_N, customerInvoiceDetail.getSequenceNumber().toString());
337            
338            KualiDecimal amountAppliedByAllOtherDocuments = 
339                customerInvoiceDetail.getAmountAppliedExcludingAnyAmountAppliedBy(paymentApplicationDocument.getDocumentNumber());
340            KualiDecimal amountAppliedByThisDocument = 
341                customerInvoiceDetail.getAmountAppliedBy(paymentApplicationDocument.getDocumentNumber());
342            KualiDecimal totalAppliedAmount = 
343                amountAppliedByAllOtherDocuments.add(amountAppliedByThisDocument);
344            
345            // Can't apply more than the total amount of the detail
346            if(!totalAppliedAmount.isLessEqual(totalFromControl)) {
347                isValid = false;
348                GlobalVariables.getMessageMap().putError(propertyName, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_EXCEEDS_AMOUNT_OUTSTANDING);
349            }
350            
351            // Can't apply a negative amount.
352            if(KualiDecimal.ZERO.isGreaterThan(amountAppliedByThisDocument)) {
353                isValid = false;
354                GlobalVariables.getMessageMap().putError(propertyName, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_GREATER_THAN_ZERO);
355            }
356            
357            // Can't apply more than the total amount outstanding on the cash control document.
358            CashControlDocument cashControlDocument = paymentApplicationDocument.getCashControlDocument();
359            if(ObjectUtils.isNotNull(cashControlDocument)) {
360                if(cashControlDocument.getCashControlTotalAmount().isLessThan(amountAppliedByThisDocument)) {
361                    isValid = false;
362                    GlobalVariables.getMessageMap().putError(propertyName, ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_APPLY_MORE_THAN_BALANCE_TO_BE_APPLIED);
363                }
364            }
365            
366            return isValid;
367        }
368        
369        /**
370         * This method validates the unapplied attribute of the document.
371         * 
372         * @param document
373         * @return
374         * @throws WorkflowException
375         */
376        public static boolean validateNonAppliedHolding(PaymentApplicationDocument applicationDocument, KualiDecimal totalFromControl) throws WorkflowException {
377            NonAppliedHolding nonAppliedHolding = applicationDocument.getNonAppliedHolding();
378            if(ObjectUtils.isNull(nonAppliedHolding)) { return true; }
379            if(StringUtils.isNotEmpty(nonAppliedHolding.getCustomerNumber())) {
380                KualiDecimal nonAppliedAmount = nonAppliedHolding.getFinancialDocumentLineAmount();
381                if(null == nonAppliedAmount) { nonAppliedAmount = KualiDecimal.ZERO; }
382                boolean isValid = totalFromControl.isGreaterEqual(nonAppliedAmount);
383                if(!isValid) {
384                    String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT;
385                    String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_EXCEED_AVAILABLE_AMOUNT;
386                    GlobalVariables.getMessageMap().putError(propertyName, errorKey);
387                }
388                // The amount of the unapplied can't exceed the remaining balance to be applied 
389                KualiDecimal totalBalanceToBeApplied = applicationDocument.getUnallocatedBalance();
390                isValid = KualiDecimal.ZERO.isLessEqual(totalBalanceToBeApplied);
391                if(!isValid) {
392                    String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT;
393                    String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_EXCEED_BALANCE_TO_BE_APPLIED;
394                    GlobalVariables.getMessageMap().putError(propertyName, errorKey);
395                }
396                
397                //  the unapplied amount cannot be negative
398                isValid = nonAppliedAmount.isPositive() || nonAppliedAmount.isZero();
399                if (!isValid) {
400                    String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT;
401                    String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_POSTIIVE;
402                    GlobalVariables.getMessageMap().putError(propertyName, errorKey);
403                }
404                return isValid;
405            } else {
406                if(ObjectUtils.isNull(nonAppliedHolding.getFinancialDocumentLineAmount()) || KualiDecimal.ZERO.equals(nonAppliedHolding.getFinancialDocumentLineAmount())) {
407                    // All's OK. Both customer number and amount are empty/null.
408                    return true;
409                } else {
410                    // Error. Customer number is empty but amount wasn't.
411                    String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_CUSTOMER_NUMBER;
412                    String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_BE_EMPTY_OR_ZERO;
413                    GlobalVariables.getMessageMap().putError(propertyName, errorKey);
414                    return false;
415                }
416            }
417        }
418        
419        /**
420         * This method sums the amounts for a List of NonInvoiceds.
421         * This is used separately from PaymentApplicationDocument.getTotalUnapplied()
422         * 
423         * @return
424         */
425        private static KualiDecimal getSumOfNonInvoiceds(List<NonInvoiced> nonInvoiceds) {
426            KualiDecimal sum = new KualiDecimal(0);
427            for(NonInvoiced nonInvoiced : nonInvoiceds) {
428                sum = sum.add(nonInvoiced.getFinancialDocumentLineAmount());
429            }
430            return sum;
431        }
432    
433    }