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