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 }