001 /* 002 * Copyright 2011 The Kuali Foundation. 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.kfs.gl.batch; 017 018 import java.io.File; 019 import java.util.HashMap; 020 import java.util.Iterator; 021 import java.util.List; 022 import java.util.Map; 023 import java.util.Set; 024 import java.util.Map.Entry; 025 026 import org.apache.commons.lang.StringUtils; 027 import org.kuali.kfs.gl.batch.service.impl.DocumentGroupData; 028 import org.kuali.kfs.gl.batch.service.impl.OriginEntryFileIterator; 029 import org.kuali.kfs.gl.batch.service.impl.OriginEntryTotals; 030 import org.kuali.kfs.gl.businessobject.CollectorDetail; 031 import org.kuali.kfs.gl.businessobject.OriginEntryFull; 032 import org.kuali.kfs.gl.businessobject.OriginEntryInformation; 033 import org.kuali.kfs.gl.report.CollectorReportData; 034 import org.kuali.kfs.gl.service.ScrubberService; 035 import org.kuali.kfs.gl.service.impl.CollectorScrubberStatus; 036 import org.kuali.kfs.gl.service.impl.ScrubberStatus; 037 import org.kuali.kfs.sys.Message; 038 import org.kuali.kfs.sys.batch.BatchSpringContext; 039 import org.kuali.kfs.sys.batch.Step; 040 import org.kuali.kfs.sys.context.ProxyUtils; 041 import org.kuali.rice.kns.service.DateTimeService; 042 import org.kuali.rice.kns.service.KualiConfigurationService; 043 import org.kuali.rice.kns.service.PersistenceService; 044 import org.springframework.aop.framework.Advised; 045 import org.springframework.aop.support.AopUtils; 046 047 048 /** 049 * This class scrubs the billing details in a collector batch. Note that all services used by this class are passed in as parameters 050 * to the constructor. NOTE: IT IS IMPERATIVE that a new instance of this class is constructed and used to parse each batch. Sharing 051 * instances to scrub multiple batches may lead to unpredictable results. 052 */ 053 public class CollectorScrubberProcess { 054 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CollectorScrubberProcess.class); 055 056 protected CollectorBatch batch; 057 protected String inputFileName; 058 protected String validFileName; 059 protected String errorFileName; 060 protected String expiredFileName; 061 protected KualiConfigurationService kualiConfigurationService; 062 protected PersistenceService persistenceService; 063 protected CollectorReportData collectorReportData; 064 protected ScrubberService scrubberService; 065 protected DateTimeService dateTimeService; 066 067 protected Set<DocumentGroupData> errorDocumentGroups; 068 protected String collectorFileDirectoryName; 069 070 /** 071 * Constructs a CollectorScrubberProcess.java. 072 * 073 * @param batch the batch to scrub 074 * @param inputGroup the origin entry group that holds all of the origin entries coming from the parsed input groups in the 075 * given batch 076 * @param validGroup the origin entry group that holds all of the origin entries coming that are in the origin entry scrubber 077 * valid group 078 * @param errorGroup the origin entry group that holds all of the origin entries coming that are in the origin entry scrubber 079 * error group 080 * @param expiredGroup are in the origin entry scrubber valid group that are in the origin entry scrubber expired group 081 * @param originEntryService the origin entry service that holds the origin entries in the batch (not necessarily the default 082 * implementation) 083 * @param originEntryGroupService the origin entry group service that holds the 3 group parameters (not necessarily the default 084 * implementation) 085 * @param kualiConfigurationService the config service 086 * @param persistenceService the persistence service 087 */ 088 public CollectorScrubberProcess(CollectorBatch batch, KualiConfigurationService kualiConfigurationService, PersistenceService persistenceService, ScrubberService scrubberService, CollectorReportData collectorReportData, DateTimeService dateTimeService, String collectorFileDirectoryName) { 089 this.batch = batch; 090 this.kualiConfigurationService = kualiConfigurationService; 091 this.persistenceService = persistenceService; 092 this.collectorReportData = collectorReportData; 093 this.scrubberService = scrubberService; 094 this.dateTimeService = dateTimeService; 095 this.collectorFileDirectoryName = collectorFileDirectoryName; 096 } 097 098 /** 099 * Scrubs the entries read in by the Collector 100 * 101 * @return a CollectorScrubberStatus object encapsulating the results of the scrubbing process 102 */ 103 public CollectorScrubberStatus scrub() { 104 // for the collector origin entry group scrub, we make sure that we're using a custom impl of the origin entry service and 105 // group service. 106 ScrubberStatus scrubberStatus = new ScrubberStatus(); 107 Step step = BatchSpringContext.getStep(CollectorScrubberStep.STEP_NAME); 108 CollectorScrubberStep collectorStep = (CollectorScrubberStep) ProxyUtils.getTargetIfProxied(step); 109 collectorStep.setScrubberStatus(scrubberStatus); 110 collectorStep.setBatch(batch); 111 collectorStep.setCollectorReportData(collectorReportData); 112 113 try { 114 step.execute(getClass().getName(), dateTimeService.getCurrentDate()); 115 } 116 catch (Exception e) { 117 LOG.error("Exception occured executing step", e); 118 throw new RuntimeException("Exception occured executing step", e); 119 } 120 121 CollectorScrubberStatus collectorScrubberStatus = new CollectorScrubberStatus(); 122 // extract the group BOs form the scrubber 123 124 // the FileName that contains all of the origin entries from the collector file 125 inputFileName = scrubberStatus.getInputFileName(); 126 collectorScrubberStatus.setInputFileName(inputFileName); 127 128 // the FileName that contains all of the origin entries from the scrubber valid FileName 129 validFileName = scrubberStatus.getValidFileName(); 130 collectorScrubberStatus.setValidFileName(validFileName); 131 132 // the FileName that contains all of the origin entries from the scrubber error FileName 133 errorFileName = scrubberStatus.getErrorFileName(); 134 collectorScrubberStatus.setErrorFileName(errorFileName); 135 136 // the FileName that contains all of the origin entries from the scrubber expired FileName (expired accounts) 137 expiredFileName = scrubberStatus.getExpiredFileName(); 138 collectorScrubberStatus.setExpiredFileName(expiredFileName); 139 140 retrieveErrorDocumentGroups(); 141 142 retrieveTotalsOnInputOriginEntriesAssociatedWithErrorGroup(); 143 144 removeInterDepartmentalBillingAssociatedWithErrorGroup(); 145 146 applyChangesToDetailsFromScrubberEdits(scrubberStatus.getUnscrubbedToScrubbedEntries()); 147 148 return collectorScrubberStatus; 149 } 150 151 /** 152 * Removes Collector IB details not associated with entries in the Collector data 153 */ 154 protected void removeInterDepartmentalBillingNotAssociatedWithInputEntries() { 155 Iterator<CollectorDetail> detailIter = batch.getCollectorDetails().iterator(); 156 while (detailIter.hasNext()) { 157 CollectorDetail detail = detailIter.next(); 158 for (OriginEntryFull inputEntry : batch.getOriginEntries()) { 159 if (!isOriginEntryRelatedToDetailRecord(inputEntry, detail)) { 160 // TODO: add reporting data 161 detailIter.remove(); 162 } 163 } 164 } 165 } 166 167 /** 168 * This method's purpose is similar to the scrubber's demerger. This method scans through all of the origin entries and removes 169 * those billing details that share the same doc number, doc type, and origination code 170 */ 171 protected void removeInterDepartmentalBillingAssociatedWithErrorGroup() { 172 int numDetailDeleted = 0; 173 Iterator<CollectorDetail> detailIter = batch.getCollectorDetails().iterator(); 174 while (detailIter.hasNext()) { 175 CollectorDetail detail = detailIter.next(); 176 for (DocumentGroupData errorDocumentGroup : errorDocumentGroups) { 177 if (errorDocumentGroup.matchesCollectorDetail(detail)) { 178 numDetailDeleted++; 179 detailIter.remove(); 180 } 181 } 182 } 183 184 collectorReportData.setNumDetailDeleted(batch, new Integer(numDetailDeleted)); 185 } 186 187 /** 188 * Determines if an origin entry is related to the given Collector detail record 189 * 190 * @param originEntry the origin entry to check 191 * @param detail the Collector detail to check against 192 * @return true if the origin entry is related, false otherwise 193 */ 194 protected boolean isOriginEntryRelatedToDetailRecord(OriginEntryInformation originEntry, CollectorDetail detail) { 195 return StringUtils.equals(originEntry.getUniversityFiscalPeriodCode(), detail.getUniversityFiscalPeriodCode()) 196 && originEntry.getUniversityFiscalYear() != null && originEntry.getUniversityFiscalYear().equals(detail.getUniversityFiscalYear()) 197 && StringUtils.equals(originEntry.getChartOfAccountsCode(), detail.getChartOfAccountsCode()) 198 && StringUtils.equals(originEntry.getAccountNumber(), detail.getAccountNumber()) 199 && StringUtils.equals(originEntry.getSubAccountNumber(), detail.getSubAccountNumber()) 200 && StringUtils.equals(originEntry.getFinancialObjectCode(), detail.getFinancialObjectCode()) 201 && StringUtils.equals(originEntry.getFinancialSubObjectCode(), detail.getFinancialSubObjectCode()) 202 && StringUtils.equals(originEntry.getFinancialSystemOriginationCode(), detail.getFinancialSystemOriginationCode()) 203 && StringUtils.equals(originEntry.getFinancialDocumentTypeCode(), detail.getFinancialDocumentTypeCode()) 204 && StringUtils.equals(originEntry.getDocumentNumber(), detail.getDocumentNumber()) 205 && StringUtils.equals(originEntry.getFinancialObjectTypeCode(), detail.getFinancialObjectTypeCode()); 206 } 207 208 /** 209 * Determines if one of the messages in the given list of errors is a fatal message 210 * 211 * @param errors a List of errors generated by the scrubber 212 * @return true if one of the errors was fatal, false otherwise 213 */ 214 private boolean hasFatal(List<Message> errors) { 215 for (Iterator<Message> iter = errors.iterator(); iter.hasNext();) { 216 Message element = iter.next(); 217 if (element.getType() == Message.TYPE_FATAL) { 218 return true; 219 } 220 } 221 return false; 222 } 223 224 /** 225 * Determines if any of the error messages in the given list are warnings 226 * 227 * @param errors a list of errors generated by the Scrubber 228 * @return true if there are any warnings in the list, false otherwise 229 */ 230 private boolean hasWarning(List<Message> errors) { 231 for (Iterator<Message> iter = errors.iterator(); iter.hasNext();) { 232 Message element = iter.next(); 233 if (element.getType() == Message.TYPE_WARNING) { 234 return true; 235 } 236 } 237 return false; 238 } 239 240 /** 241 * Updates the Collector detail with the data from a scrubbed entry 242 * 243 * @param originEntry a scrubbed origin entry 244 * @param detail a Collector detail to update 245 */ 246 protected void applyScrubberEditsToDetail(OriginEntryInformation originEntry, CollectorDetail detail) { 247 detail.setUniversityFiscalPeriodCode(originEntry.getUniversityFiscalPeriodCode()); 248 detail.setUniversityFiscalYear(originEntry.getUniversityFiscalYear()); 249 detail.setChartOfAccountsCode(originEntry.getChartOfAccountsCode()); 250 detail.setAccountNumber(originEntry.getAccountNumber()); 251 detail.setSubAccountNumber(originEntry.getSubAccountNumber()); 252 detail.setFinancialObjectCode(originEntry.getFinancialObjectCode()); 253 detail.setFinancialSubObjectCode(originEntry.getFinancialSubObjectCode()); 254 detail.setFinancialSystemOriginationCode(originEntry.getFinancialSystemOriginationCode()); 255 detail.setFinancialDocumentTypeCode(originEntry.getFinancialDocumentTypeCode()); 256 detail.setDocumentNumber(originEntry.getDocumentNumber()); 257 detail.setFinancialBalanceTypeCode(originEntry.getFinancialBalanceTypeCode()); 258 detail.setFinancialObjectTypeCode(originEntry.getFinancialObjectTypeCode()); 259 } 260 261 /** 262 * Updates all Collector details with the data from scrubbed origin entries 263 * 264 * @param unscrubbedToScrubbedEntries a Map relating original origin entries to scrubbed origin entries 265 */ 266 protected void applyChangesToDetailsFromScrubberEdits(Map<OriginEntryInformation, OriginEntryInformation> unscrubbedToScrubbedEntries) { 267 Set<Entry<OriginEntryInformation, OriginEntryInformation>> mappings = unscrubbedToScrubbedEntries.entrySet(); 268 269 int numDetailAccountValuesChanged = 0; 270 for (CollectorDetail detail : batch.getCollectorDetails()) { 271 for (Entry<OriginEntryInformation, OriginEntryInformation> mapping : mappings) { 272 OriginEntryInformation originalEntry = mapping.getKey(); 273 OriginEntryInformation scrubbedEntry = mapping.getValue(); 274 // TODO: this algorithm could be made faster using a lookup table instead of a nested loop 275 if (isOriginEntryRelatedToDetailRecord(originalEntry, detail)) { 276 if (!StringUtils.equals(originalEntry.getChartOfAccountsCode(), scrubbedEntry.getChartOfAccountsCode()) || !StringUtils.equals(originalEntry.getAccountNumber(), scrubbedEntry.getAccountNumber())) { 277 numDetailAccountValuesChanged++; 278 } 279 applyScrubberEditsToDetail(scrubbedEntry, detail); 280 break; 281 } 282 } 283 } 284 285 collectorReportData.setNumDetailAccountValuesChanged(batch, numDetailAccountValuesChanged); 286 } 287 288 /** 289 * Based on the origin entries in the origin entry scrubber-produced error group, creates a set of all {@link DocumentGroupData}s 290 * represented by those origin entries and initializes the {@link #errorDocumentGroups} variable 291 */ 292 protected void retrieveErrorDocumentGroups() { 293 File errorFile = new File( collectorFileDirectoryName + File.separator + errorFileName); 294 OriginEntryFileIterator entryIterator = new OriginEntryFileIterator(errorFile); 295 errorDocumentGroups = DocumentGroupData.getDocumentGroupDatasForTransactions(entryIterator); 296 } 297 298 /** 299 * Computes the totals of the input entries that were associated with the entries in the error group, which is created in the 300 * scrubber. These totals are reflected in the collector report data object. 301 */ 302 protected void retrieveTotalsOnInputOriginEntriesAssociatedWithErrorGroup() { 303 Map<DocumentGroupData, OriginEntryTotals> inputDocumentTotals = new HashMap<DocumentGroupData, OriginEntryTotals>(); 304 305 File inputFile = new File(collectorFileDirectoryName + File.separator + inputFileName); 306 OriginEntryFileIterator inputIterator = new OriginEntryFileIterator(inputFile); 307 308 while (inputIterator.hasNext()) { 309 OriginEntryFull originEntryFull = (OriginEntryFull) inputIterator.next(); 310 311 DocumentGroupData inputGroupData = new DocumentGroupData(originEntryFull.getDocumentNumber(), originEntryFull.getFinancialDocumentTypeCode(), originEntryFull.getFinancialSystemOriginationCode()); 312 if (errorDocumentGroups.contains(inputGroupData)) { 313 OriginEntryTotals originEntryTotals = new OriginEntryTotals(); 314 if (inputDocumentTotals.containsKey(inputGroupData)) { 315 originEntryTotals = inputDocumentTotals.get(inputGroupData); 316 } 317 318 originEntryTotals.addToTotals(originEntryFull); 319 inputDocumentTotals.put(inputGroupData, originEntryTotals); 320 } 321 } 322 323 collectorReportData.setTotalsOnInputOriginEntriesAssociatedWithErrorGroup(batch, inputDocumentTotals); 324 } 325 }