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