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    }