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}