/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.gl.batch;
import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.gl.batch.service.impl.DocumentGroupData;
import org.kuali.kfs.gl.batch.service.impl.OriginEntryFileIterator;
import org.kuali.kfs.gl.batch.service.impl.OriginEntryTotals;
import org.kuali.kfs.gl.businessobject.CollectorDetail;
import org.kuali.kfs.gl.businessobject.OriginEntryFull;
import org.kuali.kfs.gl.businessobject.OriginEntryInformation;
import org.kuali.kfs.gl.report.CollectorReportData;
import org.kuali.kfs.gl.service.ScrubberService;
import org.kuali.kfs.gl.service.impl.CollectorScrubberStatus;
import org.kuali.kfs.gl.service.impl.ScrubberStatus;
import org.kuali.kfs.sys.Message;
import org.kuali.kfs.sys.batch.BatchSpringContext;
import org.kuali.kfs.sys.batch.Step;
import org.kuali.kfs.sys.context.ProxyUtils;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.krad.service.PersistenceService;
/**
* This class scrubs the billing details in a collector batch. Note that all services used by this class are passed in as parameters
* to the constructor. NOTE: IT IS IMPERATIVE that a new instance of this class is constructed and used to parse each batch. Sharing
* instances to scrub multiple batches may lead to unpredictable results.
*/
public class CollectorScrubberProcess {
private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CollectorScrubberProcess.class);
protected CollectorBatch batch;
protected String inputFileName;
protected String validFileName;
protected String errorFileName;
protected String expiredFileName;
protected ConfigurationService kualiConfigurationService;
protected PersistenceService persistenceService;
protected CollectorReportData collectorReportData;
protected ScrubberService scrubberService;
protected DateTimeService dateTimeService;
protected Set<DocumentGroupData> errorDocumentGroups;
protected String collectorFileDirectoryName;
/**
* Constructs a CollectorScrubberProcess.java.
*
* @param batch the batch to scrub
* @param inputGroup the origin entry group that holds all of the origin entries coming from the parsed input groups in the
* given batch
* @param validGroup the origin entry group that holds all of the origin entries coming that are in the origin entry scrubber
* valid group
* @param errorGroup the origin entry group that holds all of the origin entries coming that are in the origin entry scrubber
* error group
* @param expiredGroup are in the origin entry scrubber valid group that are in the origin entry scrubber expired group
* @param originEntryService the origin entry service that holds the origin entries in the batch (not necessarily the default
* implementation)
* @param originEntryGroupService the origin entry group service that holds the 3 group parameters (not necessarily the default
* implementation)
* @param kualiConfigurationService the config service
* @param persistenceService the persistence service
*/
public CollectorScrubberProcess(CollectorBatch batch, ConfigurationService kualiConfigurationService, PersistenceService persistenceService, ScrubberService scrubberService, CollectorReportData collectorReportData, DateTimeService dateTimeService, String collectorFileDirectoryName) {
this.batch = batch;
this.kualiConfigurationService = kualiConfigurationService;
this.persistenceService = persistenceService;
this.collectorReportData = collectorReportData;
this.scrubberService = scrubberService;
this.dateTimeService = dateTimeService;
this.collectorFileDirectoryName = collectorFileDirectoryName;
}
/**
* Scrubs the entries read in by the Collector
*
* @return a CollectorScrubberStatus object encapsulating the results of the scrubbing process
*/
public CollectorScrubberStatus scrub() {
// for the collector origin entry group scrub, we make sure that we're using a custom impl of the origin entry service and
// group service.
ScrubberStatus scrubberStatus = new ScrubberStatus();
Step step = BatchSpringContext.getStep(CollectorScrubberStep.STEP_NAME);
CollectorScrubberStep collectorStep = (CollectorScrubberStep) ProxyUtils.getTargetIfProxied(step);
collectorStep.setScrubberStatus(scrubberStatus);
collectorStep.setBatch(batch);
collectorStep.setCollectorReportData(collectorReportData);
try {
step.execute(getClass().getName(), dateTimeService.getCurrentDate());
}
catch (Exception e) {
LOG.error("Exception occured executing step", e);
throw new RuntimeException("Exception occured executing step", e);
}
CollectorScrubberStatus collectorScrubberStatus = new CollectorScrubberStatus();
// extract the group BOs form the scrubber
// the FileName that contains all of the origin entries from the collector file
inputFileName = scrubberStatus.getInputFileName();
collectorScrubberStatus.setInputFileName(inputFileName);
// the FileName that contains all of the origin entries from the scrubber valid FileName
validFileName = scrubberStatus.getValidFileName();
collectorScrubberStatus.setValidFileName(validFileName);
// the FileName that contains all of the origin entries from the scrubber error FileName
errorFileName = scrubberStatus.getErrorFileName();
collectorScrubberStatus.setErrorFileName(errorFileName);
// the FileName that contains all of the origin entries from the scrubber expired FileName (expired accounts)
expiredFileName = scrubberStatus.getExpiredFileName();
collectorScrubberStatus.setExpiredFileName(expiredFileName);
retrieveErrorDocumentGroups();
retrieveTotalsOnInputOriginEntriesAssociatedWithErrorGroup();
removeInterDepartmentalBillingAssociatedWithErrorGroup();
applyChangesToDetailsFromScrubberEdits(scrubberStatus.getUnscrubbedToScrubbedEntries());
return collectorScrubberStatus;
}
/**
* Removes Collector IB details not associated with entries in the Collector data
*/
protected void removeInterDepartmentalBillingNotAssociatedWithInputEntries() {
Iterator<CollectorDetail> detailIter = batch.getCollectorDetails().iterator();
while (detailIter.hasNext()) {
CollectorDetail detail = detailIter.next();
for (OriginEntryFull inputEntry : batch.getOriginEntries()) {
if (!isOriginEntryRelatedToDetailRecord(inputEntry, detail)) {
// TODO: add reporting data
detailIter.remove();
}
}
}
}
/**
* This method's purpose is similar to the scrubber's demerger. This method scans through all of the origin entries and removes
* those billing details that share the same doc number, doc type, and origination code
*/
protected void removeInterDepartmentalBillingAssociatedWithErrorGroup() {
int numDetailDeleted = 0;
Iterator<CollectorDetail> detailIter = batch.getCollectorDetails().iterator();
while (detailIter.hasNext()) {
CollectorDetail detail = detailIter.next();
for (DocumentGroupData errorDocumentGroup : errorDocumentGroups) {
if (errorDocumentGroup.matchesCollectorDetail(detail)) {
numDetailDeleted++;
detailIter.remove();
}
}
}
collectorReportData.setNumDetailDeleted(batch, new Integer(numDetailDeleted));
}
/**
* Determines if an origin entry is related to the given Collector detail record
*
* @param originEntry the origin entry to check
* @param detail the Collector detail to check against
* @return true if the origin entry is related, false otherwise
*/
protected boolean isOriginEntryRelatedToDetailRecord(OriginEntryInformation originEntry, CollectorDetail detail) {
return StringUtils.equals(originEntry.getUniversityFiscalPeriodCode(), detail.getUniversityFiscalPeriodCode())
&& originEntry.getUniversityFiscalYear() != null && originEntry.getUniversityFiscalYear().equals(detail.getUniversityFiscalYear())
&& StringUtils.equals(originEntry.getChartOfAccountsCode(), detail.getChartOfAccountsCode())
&& StringUtils.equals(originEntry.getAccountNumber(), detail.getAccountNumber())
&& StringUtils.equals(originEntry.getSubAccountNumber(), detail.getSubAccountNumber())
&& StringUtils.equals(originEntry.getFinancialObjectCode(), detail.getFinancialObjectCode())
&& StringUtils.equals(originEntry.getFinancialSubObjectCode(), detail.getFinancialSubObjectCode())
&& StringUtils.equals(originEntry.getFinancialSystemOriginationCode(), detail.getFinancialSystemOriginationCode())
&& StringUtils.equals(originEntry.getFinancialDocumentTypeCode(), detail.getFinancialDocumentTypeCode())
&& StringUtils.equals(originEntry.getDocumentNumber(), detail.getDocumentNumber())
&& StringUtils.equals(originEntry.getFinancialObjectTypeCode(), detail.getFinancialObjectTypeCode());
}
/**
* Determines if one of the messages in the given list of errors is a fatal message
*
* @param errors a List of errors generated by the scrubber
* @return true if one of the errors was fatal, false otherwise
*/
private boolean hasFatal(List<Message> errors) {
for (Iterator<Message> iter = errors.iterator(); iter.hasNext();) {
Message element = iter.next();
if (element.getType() == Message.TYPE_FATAL) {
return true;
}
}
return false;
}
/**
* Determines if any of the error messages in the given list are warnings
*
* @param errors a list of errors generated by the Scrubber
* @return true if there are any warnings in the list, false otherwise
*/
private boolean hasWarning(List<Message> errors) {
for (Iterator<Message> iter = errors.iterator(); iter.hasNext();) {
Message element = iter.next();
if (element.getType() == Message.TYPE_WARNING) {
return true;
}
}
return false;
}
/**
* Updates the Collector detail with the data from a scrubbed entry
*
* @param originEntry a scrubbed origin entry
* @param detail a Collector detail to update
*/
protected void applyScrubberEditsToDetail(OriginEntryInformation originEntry, CollectorDetail detail) {
detail.setUniversityFiscalPeriodCode(originEntry.getUniversityFiscalPeriodCode());
detail.setUniversityFiscalYear(originEntry.getUniversityFiscalYear());
detail.setChartOfAccountsCode(originEntry.getChartOfAccountsCode());
detail.setAccountNumber(originEntry.getAccountNumber());
detail.setSubAccountNumber(originEntry.getSubAccountNumber());
detail.setFinancialObjectCode(originEntry.getFinancialObjectCode());
detail.setFinancialSubObjectCode(originEntry.getFinancialSubObjectCode());
detail.setFinancialSystemOriginationCode(originEntry.getFinancialSystemOriginationCode());
detail.setFinancialDocumentTypeCode(originEntry.getFinancialDocumentTypeCode());
detail.setDocumentNumber(originEntry.getDocumentNumber());
detail.setFinancialBalanceTypeCode(originEntry.getFinancialBalanceTypeCode());
detail.setFinancialObjectTypeCode(originEntry.getFinancialObjectTypeCode());
}
/**
* Updates all Collector details with the data from scrubbed origin entries
*
* @param unscrubbedToScrubbedEntries a Map relating original origin entries to scrubbed origin entries
*/
protected void applyChangesToDetailsFromScrubberEdits(Map<OriginEntryInformation, OriginEntryInformation> unscrubbedToScrubbedEntries) {
Set<Entry<OriginEntryInformation, OriginEntryInformation>> mappings = unscrubbedToScrubbedEntries.entrySet();
int numDetailAccountValuesChanged = 0;
for (CollectorDetail detail : batch.getCollectorDetails()) {
for (Entry<OriginEntryInformation, OriginEntryInformation> mapping : mappings) {
OriginEntryInformation originalEntry = mapping.getKey();
OriginEntryInformation scrubbedEntry = mapping.getValue();
// TODO: this algorithm could be made faster using a lookup table instead of a nested loop
if (isOriginEntryRelatedToDetailRecord(originalEntry, detail)) {
if (!StringUtils.equals(originalEntry.getChartOfAccountsCode(), scrubbedEntry.getChartOfAccountsCode()) || !StringUtils.equals(originalEntry.getAccountNumber(), scrubbedEntry.getAccountNumber())) {
numDetailAccountValuesChanged++;
}
applyScrubberEditsToDetail(scrubbedEntry, detail);
break;
}
}
}
collectorReportData.setNumDetailAccountValuesChanged(batch, numDetailAccountValuesChanged);
}
/**
* Based on the origin entries in the origin entry scrubber-produced error group, creates a set of all {@link DocumentGroupData}s
* represented by those origin entries and initializes the {@link #errorDocumentGroups} variable
*/
protected void retrieveErrorDocumentGroups() {
File errorFile = new File( collectorFileDirectoryName + File.separator + errorFileName);
OriginEntryFileIterator entryIterator = new OriginEntryFileIterator(errorFile);
errorDocumentGroups = DocumentGroupData.getDocumentGroupDatasForTransactions(entryIterator);
}
/**
* Computes the totals of the input entries that were associated with the entries in the error group, which is created in the
* scrubber. These totals are reflected in the collector report data object.
*/
protected void retrieveTotalsOnInputOriginEntriesAssociatedWithErrorGroup() {
Map<DocumentGroupData, OriginEntryTotals> inputDocumentTotals = new HashMap<DocumentGroupData, OriginEntryTotals>();
File inputFile = new File(collectorFileDirectoryName + File.separator + inputFileName);
OriginEntryFileIterator inputIterator = new OriginEntryFileIterator(inputFile);
while (inputIterator.hasNext()) {
OriginEntryFull originEntryFull = (OriginEntryFull) inputIterator.next();
DocumentGroupData inputGroupData = new DocumentGroupData(originEntryFull.getDocumentNumber(), originEntryFull.getFinancialDocumentTypeCode(), originEntryFull.getFinancialSystemOriginationCode());
if (errorDocumentGroups.contains(inputGroupData)) {
OriginEntryTotals originEntryTotals = new OriginEntryTotals();
if (inputDocumentTotals.containsKey(inputGroupData)) {
originEntryTotals = inputDocumentTotals.get(inputGroupData);
}
originEntryTotals.addToTotals(originEntryFull);
inputDocumentTotals.put(inputGroupData, originEntryTotals);
}
}
collectorReportData.setTotalsOnInputOriginEntriesAssociatedWithErrorGroup(batch, inputDocumentTotals);
}
}