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.gl.batch.service.impl;
017    
018    import java.io.File;
019    import java.io.FileInputStream;
020    import java.io.FileNotFoundException;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.io.PrintStream;
024    import java.util.Collection;
025    import java.util.HashMap;
026    import java.util.HashSet;
027    import java.util.Iterator;
028    import java.util.List;
029    import java.util.Map;
030    import java.util.Set;
031    
032    import org.apache.commons.collections.IteratorUtils;
033    import org.apache.commons.io.IOUtils;
034    import org.apache.commons.lang.StringUtils;
035    import org.apache.log4j.Logger;
036    import org.kuali.kfs.coa.businessobject.Account;
037    import org.kuali.kfs.coa.businessobject.BalanceType;
038    import org.kuali.kfs.coa.businessobject.ObjectType;
039    import org.kuali.kfs.coa.service.AccountService;
040    import org.kuali.kfs.gl.GeneralLedgerConstants;
041    import org.kuali.kfs.gl.batch.CollectorBatch;
042    import org.kuali.kfs.gl.batch.CollectorStep;
043    import org.kuali.kfs.gl.batch.service.CollectorHelperService;
044    import org.kuali.kfs.gl.batch.service.CollectorScrubberService;
045    import org.kuali.kfs.gl.businessobject.CollectorDetail;
046    import org.kuali.kfs.gl.businessobject.CollectorHeader;
047    import org.kuali.kfs.gl.businessobject.OriginEntryFull;
048    import org.kuali.kfs.gl.businessobject.OriginEntryInformation;
049    import org.kuali.kfs.gl.report.CollectorReportData;
050    import org.kuali.kfs.gl.report.PreScrubberReportData;
051    import org.kuali.kfs.gl.service.CollectorDetailService;
052    import org.kuali.kfs.gl.service.OriginEntryGroupService;
053    import org.kuali.kfs.gl.service.OriginEntryService;
054    import org.kuali.kfs.gl.service.PreScrubberService;
055    import org.kuali.kfs.gl.service.impl.CollectorScrubberStatus;
056    import org.kuali.kfs.sys.KFSConstants;
057    import org.kuali.kfs.sys.KFSKeyConstants;
058    import org.kuali.kfs.sys.KFSPropertyConstants;
059    import org.kuali.kfs.sys.KFSConstants.SystemGroupParameterNames;
060    import org.kuali.kfs.sys.batch.BatchInputFileType;
061    import org.kuali.kfs.sys.batch.service.BatchInputFileService;
062    import org.kuali.kfs.sys.context.SpringContext;
063    import org.kuali.kfs.sys.exception.ParseException;
064    import org.kuali.rice.kns.service.BusinessObjectService;
065    import org.kuali.rice.kns.service.DateTimeService;
066    import org.kuali.rice.kns.service.KualiConfigurationService;
067    import org.kuali.rice.kns.service.ParameterService;
068    import org.kuali.rice.kns.util.GlobalVariables;
069    import org.kuali.rice.kns.util.KualiDecimal;
070    import org.kuali.rice.kns.util.MessageMap;
071    import org.kuali.rice.kns.util.ObjectUtils;
072    
073    /**
074     * The base implementation of CollectorHelperService
075     * @see org.kuali.kfs.gl.batch.service.CollectorService
076     */
077    public class CollectorHelperServiceImpl implements CollectorHelperService {
078        private static Logger LOG = Logger.getLogger(CollectorHelperServiceImpl.class);
079    
080        private static final String CURRENCY_SYMBOL = "$";
081    
082        private CollectorDetailService collectorDetailService;
083        private OriginEntryService originEntryService;
084        private OriginEntryGroupService originEntryGroupService;
085        private ParameterService parameterService;
086        private KualiConfigurationService configurationService;
087        private DateTimeService dateTimeService;
088        private BatchInputFileService batchInputFileService;
089        private CollectorScrubberService collectorScrubberService;
090        private AccountService accountService;
091        private PreScrubberService preScrubberService;
092        private String batchFileDirectoryName;
093        
094        /**
095         * Parses the given file, validates the batch, stores the entries, and sends email.
096         * @param fileName - name of file to load (including path)
097         * @param group the group into which to persist the origin entries for the collector batch/file
098         * @param collectorReportData the object used to store all of the collector status information for reporting
099         * @param collectorScrubberStatuses if the collector scrubber is able to be invoked upon this collector batch, then the status
100         *        info of the collector status run is added to the end of this list
101         * @param the output stream to which to store origin entries that properly pass validation
102         * @return boolean - true if load was successful, false if errors were encountered
103         * @see org.kuali.kfs.gl.batch.service.CollectorService#loadCollectorFile(java.lang.String)
104         */
105        public boolean loadCollectorFile(String fileName, CollectorReportData collectorReportData, List<CollectorScrubberStatus> collectorScrubberStatuses, BatchInputFileType collectorInputFileType, PrintStream originEntryOutputPs) {
106            boolean isValid = true;
107    
108            MessageMap fileMessageMap = collectorReportData.getMessageMapForFileName(fileName);
109            
110            List<CollectorBatch> batches = doCollectorFileParse(fileName, fileMessageMap, collectorInputFileType, collectorReportData);
111            for (int i = 0; i < batches.size(); i++) {
112                CollectorBatch collectorBatch = batches.get(i);
113    
114                collectorBatch.setBatchName(fileName + " Batch " + String.valueOf(i + 1));
115                collectorReportData.addBatch(collectorBatch);
116                
117                isValid &= loadCollectorBatch(collectorBatch, fileName, i + 1, collectorReportData, collectorScrubberStatuses, collectorInputFileType, originEntryOutputPs);
118            }
119            return isValid;
120        }
121            
122        protected boolean loadCollectorBatch(CollectorBatch batch, String fileName, int batchIndex, CollectorReportData collectorReportData, List<CollectorScrubberStatus> collectorScrubberStatuses, BatchInputFileType collectorInputFileType, PrintStream originEntryOutputPs) {
123            boolean isValid = true;
124            
125            MessageMap messageMap = batch.getMessageMap();
126            // terminate if there were parse errors
127            if (!messageMap.isEmpty()) {
128                isValid = false;
129            }
130    
131            if (isValid) {
132                collectorReportData.setNumInputDetails(batch);
133                // check totals
134                isValid = checkTrailerTotals(batch, collectorReportData, messageMap);
135            }
136    
137            // do validation, base collector files rules and total checks
138            if (isValid) {
139                isValid = performValidation(batch, messageMap);
140            }
141    
142            if (isValid) {
143                // mark batch as valid
144                collectorReportData.markValidationStatus(batch, true);
145                
146                prescrubParsedCollectorBatch(batch, collectorReportData);
147                
148                String collectorFileDirectoryName = collectorInputFileType.getDirectoryPath();
149                // create a input file for scrubber
150                String collectorInputFileNameForScrubber = batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_BACKUP_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
151                PrintStream inputFilePs = null;
152                try {
153                    inputFilePs = new PrintStream(collectorInputFileNameForScrubber);
154                
155                    for (OriginEntryFull entry : batch.getOriginEntries()){
156                        inputFilePs.printf("%s\n", entry.getLine());    
157                    }
158                } catch (IOException e) {
159                    throw new RuntimeException("loadCollectorFile Stopped: " + e.getMessage(), e);
160                } finally {
161                    IOUtils.closeQuietly(inputFilePs);
162                }
163                
164                CollectorScrubberStatus collectorScrubberStatus = collectorScrubberService.scrub(batch, collectorReportData, collectorFileDirectoryName);
165                collectorScrubberStatuses.add(collectorScrubberStatus);
166                processInterDepartmentalBillingAmounts(batch);
167    
168                // store origin group, entries, and collector detairs
169                String collectorDemergerOutputFileName = batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_DEMERGER_VAILD_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;  
170                batch.setDefaultsAndStore(collectorReportData, collectorDemergerOutputFileName, originEntryOutputPs);
171                collectorReportData.incrementNumPersistedBatches();
172            }
173            else {
174                collectorReportData.incrementNumNonPersistedBatches();
175                collectorReportData.incrementNumNotPersistedOriginEntryRecords(batch.getOriginEntries().size());
176                collectorReportData.incrementNumNotPersistedCollectorDetailRecords(batch.getCollectorDetails().size());
177                // mark batch as invalid
178                collectorReportData.markValidationStatus(batch, false);
179            }
180    
181            return isValid;
182        }
183    
184        /**
185         * After a parse error, tries to go through the file to see if the email address can be determined. This method will not throw
186         * an exception.
187         * 
188         * It's not doing much right now, just returning null
189         * 
190         * @param fileName the name of the file that a parsing error occurred on
191         * @return the email from the file
192         */
193        protected String attemptToParseEmailAfterParseError(String fileName) {
194            return null;
195        }
196    
197        /**
198         * Calls batch input service to parse the xml contents into an object. Any errors will be contained in GlobalVariables.MessageMap
199         * 
200         * @param fileName the name of the file to parse
201         * @param MessageMap a map of errors resultant from the parsing
202         * @return the CollectorBatch of details parsed from the file
203         */
204        protected List<CollectorBatch> doCollectorFileParse(String fileName, MessageMap messageMap, BatchInputFileType collectorInputFileType, CollectorReportData collectorReportData) {
205    
206            InputStream inputStream = null;
207            try {
208                inputStream = new FileInputStream(fileName);
209            }
210            catch (FileNotFoundException e) {
211                LOG.error("file to parse not found " + fileName, e);
212                collectorReportData.markUnparsableFileNames(fileName);
213                throw new RuntimeException("Cannot find the file requested to be parsed " + fileName + " " + e.getMessage(), e);
214            }
215            catch (RuntimeException e) {
216                collectorReportData.markUnparsableFileNames(fileName);
217                throw e;
218            }
219    
220            List<CollectorBatch> parsedObject = null;
221            try {
222                byte[] fileByteContent = IOUtils.toByteArray(inputStream);
223                parsedObject = (List<CollectorBatch>) batchInputFileService.parse(collectorInputFileType, fileByteContent);
224            }
225            catch (IOException e) {
226                LOG.error("error while getting file bytes:  " + e.getMessage(), e);
227                collectorReportData.markUnparsableFileNames(fileName);
228                throw new RuntimeException("Error encountered while attempting to get file bytes: " + e.getMessage(), e);
229            }
230            catch (ParseException e1) {
231                LOG.error("errors parsing file " + e1.getMessage(), e1);
232                collectorReportData.markUnparsableFileNames(fileName);
233                messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_BATCH_UPLOAD_PARSING_XML, new String[] { e1.getMessage() });
234            }
235            catch (RuntimeException e) {
236                collectorReportData.markUnparsableFileNames(fileName);
237                throw e;
238            }
239    
240            return parsedObject;
241        }
242    
243        protected void prescrubParsedCollectorBatch(CollectorBatch collectorBatch, CollectorReportData collectorReportData) {
244            if (preScrubberService.deriveChartOfAccountsCodeIfSpaces()) {
245                PreScrubberReportData preScrubberReportData = collectorReportData.getPreScrubberReportData();
246                
247                int inputRecords = collectorBatch.getOriginEntries().size();
248                Set<String> noChartCodesCache = new HashSet<String>();
249                Set<String> multipleChartCodesCache = new HashSet<String>();
250                Map<String, String> accountNumberToChartCodeCache = new HashMap<String, String>();
251                
252                Iterator<Object> originEntryAndDetailIterator = IteratorUtils.chainedIterator(collectorBatch.getOriginEntries().iterator(), collectorBatch.getCollectorDetails().iterator());
253                while (originEntryAndDetailIterator.hasNext()) {
254                    Object originEntryOrDetail = originEntryAndDetailIterator.next();
255                    if (StringUtils.isBlank(extractChartOfAccountsCode(originEntryOrDetail))) {
256                        String accountNumber = extractAccountNumber(originEntryOrDetail);
257                        
258                        boolean nonExistent = false;
259                        boolean multipleFound = false;
260                        String chartOfAccountsCode = null;
261                        
262                        if (noChartCodesCache.contains(accountNumber)) {
263                            nonExistent = true;
264                        }
265                        else if (multipleChartCodesCache.contains(accountNumber)) {
266                            multipleFound = true;
267                        }
268                        else if (accountNumberToChartCodeCache.containsKey(accountNumber)) {
269                            chartOfAccountsCode = accountNumberToChartCodeCache.get(accountNumber);
270                        }
271                        else {
272                            Collection<Account> accounts = accountService.getAccountsForAccountNumber(accountNumber);
273                            if (accounts.size() == 1) {
274                                chartOfAccountsCode = accounts.iterator().next().getChartOfAccountsCode();
275                                accountNumberToChartCodeCache.put(accountNumber, chartOfAccountsCode);
276                            }
277                            else if (accounts.size() == 0) {
278                                noChartCodesCache.add(accountNumber);
279                                nonExistent = true;
280                            }
281                            else {
282                                multipleChartCodesCache.add(accountNumber);
283                                multipleFound = true;
284                            }
285                        }
286                        
287                        if (!nonExistent && !multipleFound) {
288                            setChartOfAccountsCode(originEntryOrDetail, chartOfAccountsCode);
289                        }
290                    }
291                }
292                
293                preScrubberReportData.getAccountsWithMultipleCharts().addAll(multipleChartCodesCache);
294                preScrubberReportData.getAccountsWithNoCharts().addAll(noChartCodesCache);
295                preScrubberReportData.setInputRecords(preScrubberReportData.getInputRecords() + inputRecords);
296                preScrubberReportData.setOutputRecords(preScrubberReportData.getOutputRecords() + inputRecords);
297            }
298        }
299    
300        protected String extractChartOfAccountsCode(Object originEntryOrDetail) {
301            if (originEntryOrDetail instanceof OriginEntryInformation)
302                return ((OriginEntryInformation) originEntryOrDetail).getChartOfAccountsCode(); 
303            return ((CollectorDetail) originEntryOrDetail).getChartOfAccountsCode();
304        }
305        
306        protected String extractAccountNumber(Object originEntryOrDetail) {
307            if (originEntryOrDetail instanceof OriginEntryInformation)
308                return ((OriginEntryInformation) originEntryOrDetail).getAccountNumber(); 
309            return ((CollectorDetail) originEntryOrDetail).getAccountNumber();
310        }
311        
312        protected void setChartOfAccountsCode(Object originEntryOrDetail, String chartOfAccountsCode) {
313            if (originEntryOrDetail instanceof OriginEntryInformation)
314                ((OriginEntryInformation) originEntryOrDetail).setChartOfAccountsCode(chartOfAccountsCode);
315            else
316                ((CollectorDetail) originEntryOrDetail).setChartOfAccountsCode(chartOfAccountsCode);
317        }
318        
319        /**
320         * Validates the contents of a parsed file.
321         * 
322         * @param batch - batch to validate
323         * @return boolean - true if validation was OK, false if there were errors
324         * @see org.kuali.kfs.gl.batch.service.CollectorHelperService#performValidation(org.kuali.kfs.gl.batch.CollectorBatch)
325         */
326        public boolean performValidation(CollectorBatch batch) {
327            return performValidation(batch, GlobalVariables.getMessageMap());
328        }
329    
330        /**
331         * Performs the following checks on the collector batch: Any errors will be contained in GlobalVariables.MessageMap
332         * 
333         * @param batch - batch to validate
334         * @param MessageMap the map into which to put errors encountered during validation
335         * @return boolean - true if validation was successful, false it not
336         */
337        protected boolean performValidation(CollectorBatch batch, MessageMap MessageMap) {
338            boolean valid = performCollectorHeaderValidation(batch, MessageMap);
339            
340            performUppercasing(batch);
341    
342            boolean performDuplicateHeaderCheck = parameterService.getIndicatorParameter(CollectorStep.class, SystemGroupParameterNames.COLLECTOR_PERFORM_DUPLICATE_HEADER_CHECK);
343            if (valid && performDuplicateHeaderCheck) {
344                valid = duplicateHeaderCheck(batch, MessageMap);
345            }
346            if (valid) {
347                valid = checkForMixedDocumentTypes(batch, MessageMap);
348            }
349    
350            if (valid) {
351                valid = checkForMixedBalanceTypes(batch, MessageMap);
352            }
353    
354            if (valid) {
355                valid = checkDetailKeys(batch, MessageMap);
356            }
357    
358            return valid;
359        }
360        
361        /**
362         * Uppercases sub-account, sub-object, and project fields
363         * 
364         * @param batch CollectorBatch with data to uppercase
365         */
366        protected void performUppercasing(CollectorBatch batch) {
367            for (OriginEntryFull originEntry : batch.getOriginEntries()) {
368                if (StringUtils.isNotBlank(originEntry.getSubAccountNumber())) {
369                    originEntry.setSubAccountNumber(originEntry.getSubAccountNumber().toUpperCase());
370                }
371    
372                if (StringUtils.isNotBlank(originEntry.getFinancialSubObjectCode())) {
373                    originEntry.setFinancialSubObjectCode(originEntry.getFinancialSubObjectCode().toUpperCase());
374                }
375    
376                if (StringUtils.isNotBlank(originEntry.getProjectCode())) {
377                    originEntry.setProjectCode(originEntry.getProjectCode().toUpperCase());
378                }
379            }
380    
381            for (CollectorDetail detail : batch.getCollectorDetails()) {
382                if (StringUtils.isNotBlank(detail.getSubAccountNumber())) {
383                    detail.setSubAccountNumber(detail.getSubAccountNumber().toUpperCase());
384                }
385    
386                if (StringUtils.isNotBlank(detail.getFinancialSubObjectCode())) {
387                    detail.setFinancialSubObjectCode(detail.getFinancialSubObjectCode().toUpperCase());
388                }
389            }
390        }
391    
392        protected boolean performCollectorHeaderValidation(CollectorBatch batch, MessageMap messageMap) {
393            if (batch.isHeaderlessBatch()) {
394                // if it's a headerless batch, don't validate the header, but it's still an error
395                return false;
396            }
397            boolean valid = true;
398            if (StringUtils.isBlank(batch.getChartOfAccountsCode())) {
399                valid = false;
400                messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_CHART_CODE_REQUIRED);
401            }
402            if (StringUtils.isBlank(batch.getOrganizationCode())) {
403                valid = false;
404                messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_ORGANIZATION_CODE_REQUIRED);
405            }
406            if (StringUtils.isBlank(batch.getCampusCode())) {
407                valid = false;
408                messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_CAMPUS_CODE_REQUIRED);
409            }
410            if (StringUtils.isBlank(batch.getPhoneNumber())) {
411                valid = false;
412                messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_PHONE_NUMBER_REQUIRED);
413            }
414            if (StringUtils.isBlank(batch.getMailingAddress())) {
415                valid = false;
416                messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_MAILING_ADDRESS_REQUIRED);
417            }
418            if (StringUtils.isBlank(batch.getDepartmentName())) {
419                valid = false;
420                messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_DEPARTMENT_NAME_REQUIRED);
421            }
422            return valid;
423        }
424    
425        /**
426         * Modifies the amounts in the ID Billing Detail rows, depending on specific business rules. For this default implementation,
427         * see the {@link #negateAmountIfNecessary(InterDepartmentalBilling, BalanceTyp, ObjectType, CollectorBatch)} method to see how
428         * the billing detail amounts are modified.
429         * 
430         * @param batch a CollectorBatch to process
431         */
432        protected void processInterDepartmentalBillingAmounts(CollectorBatch batch) {
433            for (CollectorDetail collectorDetail : batch.getCollectorDetails()) {
434                String balanceTypeCode = getBalanceTypeCode(collectorDetail, batch);
435    
436                BalanceType balanceTyp = new BalanceType();
437                balanceTyp.setFinancialBalanceTypeCode(balanceTypeCode);
438                balanceTyp = (BalanceType) SpringContext.getBean(BusinessObjectService.class).retrieve(balanceTyp);
439                if (balanceTyp == null) {
440                    // no balance type in db
441                    LOG.info("No balance type code found for ID billing record. " + collectorDetail);
442                    continue;
443                }
444    
445                collectorDetail.refreshReferenceObject(KFSPropertyConstants.FINANCIAL_OBJECT);
446                if (collectorDetail.getFinancialObject() == null) {
447                    // no object code in db
448                    LOG.info("No object code found for ID billing record. " + collectorDetail);
449                    continue;
450                }
451                ObjectType objectType = collectorDetail.getFinancialObject().getFinancialObjectType();
452    
453                /** Commented out for KULRNE-5922 */
454                // negateAmountIfNecessary(collectorDetail, balanceTyp, objectType, batch);
455            }
456        }
457    
458        /**
459         * Negates the amount of the internal departmental billing detail record if necessary. For this default implementation, if the
460         * balance type's offset indicator is yes and the object type has a debit indicator, then the amount is negated.
461         * 
462         * @param collectorDetail the collector detail
463         * @param balanceTyp the balance type
464         * @param objectType the object type
465         * @param batch the patch to which the interDepartmentalBilling parameter belongs
466         */
467        protected void negateAmountIfNecessary(CollectorDetail collectorDetail, BalanceType balanceTyp, ObjectType objectType, CollectorBatch batch) {
468            if (balanceTyp != null && objectType != null) {
469                if (balanceTyp.isFinancialOffsetGenerationIndicator()) {
470                    if (KFSConstants.GL_DEBIT_CODE.equals(objectType.getFinObjectTypeDebitcreditCd())) {
471                        KualiDecimal amount = collectorDetail.getCollectorDetailItemAmount();
472                        amount = amount.negated();
473                        collectorDetail.setCollectorDetailItemAmount(amount);
474                    }
475                }
476            }
477        }
478    
479        /**
480         * Returns the balance type code for the interDepartmentalBilling record. This default implementation will look into the system
481         * parameters to determine the balance type
482         * 
483         * @param interDepartmentalBilling a inter departmental billing detail record
484         * @param batch the batch to which the interDepartmentalBilling billing belongs
485         * @return the balance type code for the billing detail
486         */
487        protected String getBalanceTypeCode(CollectorDetail collectorDetail, CollectorBatch batch) {
488            return collectorDetail.getFinancialBalanceTypeCode();
489        }
490    
491        /**
492         * Checks header against previously loaded batch headers for a duplicate submission.
493         * 
494         * @param batch - batch to check
495         * @return true if header if OK, false if header was used previously
496         */
497        protected boolean duplicateHeaderCheck(CollectorBatch batch, MessageMap messageMap) {
498            boolean validHeader = true;
499    
500            CollectorHeader foundHeader = batch.retrieveDuplicateHeader();
501    
502            if (foundHeader != null) {
503                LOG.error("batch header was matched to a previously loaded batch");
504                messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.DUPLICATE_BATCH_HEADER);
505    
506                validHeader = false;
507            }
508    
509            return validHeader;
510        }
511    
512        /**
513         * Iterates through the origin entries and builds a map on the document types. Then checks there was only one document type
514         * found.
515         * 
516         * @param batch - batch to check document types
517         * @return true if there is only one document type, false if multiple document types were found.
518         */
519        protected boolean checkForMixedDocumentTypes(CollectorBatch batch, MessageMap MessageMap) {
520            boolean docTypesNotMixed = true;
521    
522            Set batchDocumentTypes = new HashSet();
523            for (OriginEntryFull entry : batch.getOriginEntries()) {
524                batchDocumentTypes.add(entry.getFinancialDocumentTypeCode());
525            }
526    
527            if (batchDocumentTypes.size() > 1) {
528                LOG.error("mixed document types found in batch");
529                MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.MIXED_DOCUMENT_TYPES);
530    
531                docTypesNotMixed = false;
532            }
533    
534            return docTypesNotMixed;
535        }
536    
537        /**
538         * Iterates through the origin entries and builds a map on the balance types. Then checks there was only one balance type found.
539         * 
540         * @param batch - batch to check balance types
541         * @return true if there is only one balance type, false if multiple balance types were found
542         */
543        protected boolean checkForMixedBalanceTypes(CollectorBatch batch, MessageMap MessageMap) {
544            boolean balanceTypesNotMixed = true;
545    
546            Set balanceTypes = new HashSet();
547            for (OriginEntryFull entry : batch.getOriginEntries()) {
548                balanceTypes.add(entry.getFinancialBalanceTypeCode());
549            }
550    
551            if (balanceTypes.size() > 1) {
552                LOG.error("mixed balance types found in batch");
553                MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.MIXED_BALANCE_TYPES);
554    
555                balanceTypesNotMixed = false;
556            }
557    
558            return balanceTypesNotMixed;
559        }
560    
561        /**
562         * Verifies each detail (id billing) record key has an corresponding gl entry in the same batch. The key is built by joining the
563         * values of chart of accounts code, account number, sub account number, object code, and sub object code.
564         * 
565         * @param batch - batch to validate
566         * @return true if all detail records had matching keys, false otherwise
567         */
568        protected boolean checkDetailKeys(CollectorBatch batch, MessageMap MessageMap) {
569            boolean detailKeysFound = true;
570    
571            // build a Set of keys from the gl entries to compare with
572            Set<String> glEntryKeys = new HashSet<String>();
573            for (OriginEntryFull entry : batch.getOriginEntries()) {
574                glEntryKeys.add(generateOriginEntryMatchingKey(entry, ", "));
575            }
576    
577            for (CollectorDetail collectorDetail : batch.getCollectorDetails()) {
578                String collectorDetailKey = generateCollectorDetailMatchingKey(collectorDetail, ", ");
579                if (!glEntryKeys.contains(collectorDetailKey)) {
580                    LOG.error("found detail key without a matching gl entry key " + collectorDetailKey);
581                    MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.NONMATCHING_DETAIL_KEY, collectorDetailKey);
582    
583                    detailKeysFound = false;
584                }
585            }
586    
587            return detailKeysFound;
588        }
589    
590        /**
591         * Generates a String representation of the OriginEntryFull's primary key
592         * 
593         * @param entry origin entry to get key from
594         * @param delimiter the String delimiter to separate parts of the key
595         * @return the key as a String
596         */
597        protected String generateOriginEntryMatchingKey(OriginEntryFull entry, String delimiter) {
598            return StringUtils.join(new String[] { ObjectUtils.isNull(entry.getUniversityFiscalYear()) ? "" : entry.getUniversityFiscalYear().toString(), entry.getUniversityFiscalPeriodCode(), entry.getChartOfAccountsCode(), entry.getAccountNumber(), entry.getSubAccountNumber(), entry.getFinancialObjectCode(), entry.getFinancialSubObjectCode(), entry.getFinancialObjectTypeCode(), entry.getDocumentNumber(), entry.getFinancialDocumentTypeCode(), entry.getFinancialSystemOriginationCode() }, delimiter);
599        }
600    
601        /**
602         * Generates a String representation of the CollectorDetail's primary key
603         * 
604         * @param collectorDetail collector detail to get key from
605         * @param delimiter the String delimiter to separate parts of the key
606         * @return the key as a String
607         */
608        protected String generateCollectorDetailMatchingKey(CollectorDetail collectorDetail, String delimiter) {
609            return StringUtils.join(new String[] { ObjectUtils.isNull(collectorDetail.getUniversityFiscalYear()) ? "" : collectorDetail.getUniversityFiscalYear().toString(), collectorDetail.getUniversityFiscalPeriodCode(), collectorDetail.getChartOfAccountsCode(), collectorDetail.getAccountNumber(), collectorDetail.getSubAccountNumber(), collectorDetail.getFinancialObjectCode(), collectorDetail.getFinancialSubObjectCode(), collectorDetail.getFinancialObjectTypeCode(), collectorDetail.getDocumentNumber(), collectorDetail.getFinancialDocumentTypeCode(), collectorDetail.getFinancialSystemOriginationCode() }, delimiter);
610        }
611    
612        /**
613         * Checks the batch total line count and amounts against the trailer. Any errors will be contained in GlobalVariables.MessageMap
614         * 
615         * @param batch batch to check totals for
616         * @param collectorReportData collector report data (optional)
617         * @see org.kuali.kfs.gl.batch.service.CollectorHelperService#checkTrailerTotals(org.kuali.kfs.gl.batch.CollectorBatch,
618         *      org.kuali.kfs.gl.report.CollectorReportData)
619         */
620        public boolean checkTrailerTotals(CollectorBatch batch, CollectorReportData collectorReportData) {
621            return checkTrailerTotals(batch, collectorReportData, GlobalVariables.getMessageMap());
622        }
623    
624        /**
625         * Checks the batch total line count and amounts against the trailer. Any errors will be contained in GlobalVariables.MessageMap
626         * 
627         * @param batch - batch to check totals for
628         * @return boolean - true if validation was successful, false it not
629         */
630        protected boolean checkTrailerTotals(CollectorBatch batch, CollectorReportData collectorReportData, MessageMap MessageMap) {
631            boolean trailerTotalsMatch = true;
632    
633            int actualRecordCount = batch.getOriginEntries().size() + batch.getCollectorDetails().size();
634            if (actualRecordCount != batch.getTotalRecords()) {
635                LOG.error("trailer check on total count did not pass, expected count: " + String.valueOf(batch.getTotalRecords()) + ", actual count: " + String.valueOf(actualRecordCount));
636                MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.TRAILER_ERROR_COUNTNOMATCH, String.valueOf(batch.getTotalRecords()), String.valueOf(actualRecordCount));
637                trailerTotalsMatch = false;
638            }
639    
640            OriginEntryTotals totals = batch.getOriginEntryTotals();
641            
642            if (batch.getOriginEntries().size() == 0) {
643                if (!KualiDecimal.ZERO.equals(batch.getTotalAmount())) {
644                    LOG.error("trailer total should be zero when there are no origin entries");
645                    MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.TRAILER_ERROR_AMOUNT_SHOULD_BE_ZERO);
646                }
647                return false;
648            }
649    
650            // retrieve document types that balance by equal debits and credits
651            String[] documentTypes = parameterService.getParameterValues(CollectorStep.class, KFSConstants.SystemGroupParameterNames.COLLECTOR_EQUAL_DC_TOTAL_DOCUMENT_TYPES).toArray(new String[] {});
652    
653            boolean equalDebitCreditTotal = false;
654            for (int i = 0; i < documentTypes.length; i++) {
655                String documentType = StringUtils.remove(documentTypes[i], "*");
656                if (batch.getOriginEntries().get(0).getFinancialDocumentTypeCode().startsWith(documentType.toUpperCase()) && KFSConstants.BALANCE_TYPE_ACTUAL.equals(batch.getOriginEntries().get(0).getFinancialBalanceTypeCode())) {
657                    equalDebitCreditTotal = true;
658                }
659            }
660    
661            if (equalDebitCreditTotal) {
662                // credits must equal debits must equal total trailer amount
663                if (!totals.getCreditAmount().equals(totals.getDebitAmount()) || !totals.getCreditAmount().equals(batch.getTotalAmount())) {
664                    LOG.error("trailer check on total amount did not pass, debit should equal credit, should equal trailer total");
665                    MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.TRAILER_ERROR_AMOUNTNOMATCH1, totals.getCreditAmount().toString(), totals.getDebitAmount().toString(), batch.getTotalAmount().toString());
666                    trailerTotalsMatch = false;
667                }
668            }
669            else {
670                // credits plus debits plus other amount must equal trailer
671                KualiDecimal totalGlEntries = totals.getCreditAmount().add(totals.getDebitAmount()).add(totals.getOtherAmount());
672                if (!totalGlEntries.equals(batch.getTotalAmount())) {
673                    LOG.error("trailer check on total amount did not pass, sum of gl entry amounts should equal trailer total");
674                    MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.TRAILER_ERROR_AMOUNTNOMATCH2, totalGlEntries.toString(), batch.getTotalAmount().toString());
675                    trailerTotalsMatch = false;
676                }
677            }
678    
679            return trailerTotalsMatch;
680        }
681    
682        public void setCollectorDetailService(CollectorDetailService collectorDetailService) {
683            this.collectorDetailService = collectorDetailService;
684        }
685    
686        public void setOriginEntryGroupService(OriginEntryGroupService originEntryGroupService) {
687            this.originEntryGroupService = originEntryGroupService;
688        }
689    
690        public void setOriginEntryService(OriginEntryService originEntryService) {
691            this.originEntryService = originEntryService;
692        }
693    
694        /**
695         * Returns the name of the directory where Collector files are saved
696         * 
697         * @return the name of the staging directory
698         */
699        public String getStagingDirectory() {
700            return configurationService.getPropertyString(KFSConstants.GL_COLLECTOR_STAGING_DIRECTORY);
701        }
702    
703        public void setDateTimeService(DateTimeService dateTimeService) {
704            this.dateTimeService = dateTimeService;
705        }
706    
707        public void setBatchInputFileService(BatchInputFileService batchInputFileService) {
708            this.batchInputFileService = batchInputFileService;
709        }
710    
711        /**
712         * Sets the collectorScrubberService attribute value.
713         * 
714         * @param collectorScrubberService The collectorScrubberService to set.
715         */
716        public void setCollectorScrubberService(CollectorScrubberService collectorScrubberService) {
717            this.collectorScrubberService = collectorScrubberService;
718        }
719    
720        public void setConfigurationService(KualiConfigurationService configurationService) {
721            this.configurationService = configurationService;
722        }
723    
724        public void setParameterService(ParameterService parameterService) {
725            this.parameterService = parameterService;
726        }
727    
728        /**
729         * Sets the batchFileDirectoryName attribute value.
730         * @param batchFileDirectoryName The batchFileDirectoryName to set.
731         */
732        public void setBatchFileDirectoryName(String batchFileDirectoryName) {
733            this.batchFileDirectoryName = batchFileDirectoryName;
734        }
735    
736        /**
737         * Sets the accountService attribute value.
738         * @param accountService The accountService to set.
739         */
740        public void setAccountService(AccountService accountService) {
741            this.accountService = accountService;
742        }
743    
744        /**
745         * Sets the preScrubberService attribute value.
746         * @param preScrubberService The preScrubberService to set.
747         */
748        public void setPreScrubberService(PreScrubberService preScrubberService) {
749            this.preScrubberService = preScrubberService;
750        }
751    }