/* * 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.service.impl; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Formattable; import java.util.Formatter; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.gl.batch.CollectorBatch; import org.kuali.kfs.gl.batch.CollectorStep; import org.kuali.kfs.gl.batch.service.CollectorReportService; import org.kuali.kfs.gl.businessobject.DemergerReportData; import org.kuali.kfs.gl.businessobject.OriginEntryFull; import org.kuali.kfs.gl.businessobject.Transaction; import org.kuali.kfs.gl.report.CollectorReportData; import org.kuali.kfs.gl.report.LedgerSummaryReport; import org.kuali.kfs.gl.report.PreScrubberReport; import org.kuali.kfs.gl.report.Summary; import org.kuali.kfs.gl.service.PreScrubberService; import org.kuali.kfs.gl.service.ScrubberReportData; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.KFSConstants.SystemGroupParameterNames; import org.kuali.kfs.sys.KFSKeyConstants; import org.kuali.kfs.sys.Message; import org.kuali.kfs.sys.service.ReportWriterService; import org.kuali.rice.core.api.config.property.ConfigurationService; import org.kuali.rice.core.api.datetime.DateTimeService; import org.kuali.rice.core.api.mail.MailMessage; import org.kuali.rice.core.api.util.type.KualiDecimal; import org.kuali.rice.core.web.format.CurrencyFormatter; import org.kuali.rice.coreservice.framework.parameter.ParameterService; import org.kuali.rice.krad.service.MailService; import org.kuali.rice.krad.util.ErrorMessage; import org.kuali.rice.krad.util.MessageMap; /** * The base implementation of the CollectorReportService */ public class CollectorReportServiceImpl implements CollectorReportService { private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CollectorReportServiceImpl.class); protected DateTimeService dateTimeService; protected ParameterService parameterService; protected ConfigurationService configurationService; protected MailService mailService; protected PreScrubberService preScrubberService; protected ReportWriterService collectorReportWriterService; /** * Sends out e-mails about the validation and demerger of the Collector run * * @param collectorReportData data gathered from the run of the Collector * @see org.kuali.kfs.gl.batch.service.CollectorReportService#sendEmails(org.kuali.kfs.gl.report.CollectorReportData) */ @Override public void sendEmails(CollectorReportData collectorReportData) { // send out the validation status messages Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches(); while (batchIter.hasNext()) { CollectorBatch batch = batchIter.next(); sendValidationEmail(batch, collectorReportData); sendDemergerEmail(batch, collectorReportData); } sendEmailSendFailureNotice(collectorReportData); } /** * Generates the reports about a given Collector run * * @param collectorReportData data gathered from the run of the Collector * @see org.kuali.kfs.gl.batch.service.CollectorReportService#generateCollectorRunReports(org.kuali.kfs.gl.report.CollectorReportData) */ @Override public void generateCollectorRunReports(CollectorReportData collectorReportData) { appendCollectorHeaderInformation(collectorReportData); appendPreScrubberReport(collectorReportData); appendScrubberReport(collectorReportData); appendDemergerReport(collectorReportData); appendDeletedOriginEntryAndDetailReport(collectorReportData); appendDetailChangedAccountReport(collectorReportData); appendLedgerReport(collectorReportData); } /** * Appends Collector header information to the report writer * * @param collectorReportData data gathered from the run of the Collector */ protected void appendCollectorHeaderInformation(CollectorReportData collectorReportData) { Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches(); OriginEntryTotals aggregateOriginEntryTotals = new OriginEntryTotals(); int aggregateTotalRecordsCountFromTrailer = 0; int aggregateNumInputDetails = 0; int aggregateNumSavedDetails = 0; if (!collectorReportData.getAllUnparsableFileNames().isEmpty()) { collectorReportWriterService.writeFormattedMessageLine("The following files could not be parsed:\n\n"); for (String unparsableFileName : collectorReportData.getAllUnparsableFileNames()) { List<String> batchErrors = translateErrorsFromMessageMap(collectorReportData.getMessageMapForFileName(unparsableFileName)); collectorReportWriterService.writeFormattedMessageLine(" " + unparsableFileName + "\n"); for (String errorMessage : batchErrors) { collectorReportWriterService.writeFormattedMessageLine(" - ERROR MESSAGE: " + errorMessage); } } } while (batchIter.hasNext()) { CollectorBatch batch = batchIter.next(); StringBuilder buf = new StringBuilder(); OriginEntryTotals batchOriginEntryTotals = batch.getOriginEntryTotals(); appendHeaderInformation(buf, batch, collectorReportData); appendTotalsInformation(buf, batch); List<String> errorMessages = translateErrorsFromMessageMap(batch.getMessageMap()); aggregateTotalRecordsCountFromTrailer += batch.getTotalRecords(); // if batch is valid add up totals if (collectorReportData.isBatchValid(batch)) { if (batchOriginEntryTotals != null) { aggregateOriginEntryTotals.incorporateTotals(batchOriginEntryTotals); } Integer batchNumInputDetails = collectorReportData.getNumInputDetails(batch); if (batchNumInputDetails != null) { aggregateNumInputDetails += batchNumInputDetails; } Integer batchNumSavedDetails = collectorReportData.getNumSavedDetails(batch); if (batchNumSavedDetails != null) { aggregateNumSavedDetails += batchNumSavedDetails; } } collectorReportWriterService.writeFormattedMessageLine("Header *********************************************************************"); collectorReportWriterService.writeMultipleFormattedMessageLines(buf.toString()); String validationErrors = getValidationStatus(errorMessages, false, 15); if (StringUtils.isNotBlank(validationErrors)) { collectorReportWriterService.writeMultipleFormattedMessageLines(validationErrors); } } collectorReportWriterService.writeNewLines(2); collectorReportWriterService.writeFormattedMessageLine("***** Totals for Creation of GLE Data *****"); collectorReportWriterService.writeFormattedMessageLine(" Total Records Read %09d", aggregateTotalRecordsCountFromTrailer); collectorReportWriterService.writeFormattedMessageLine(" Total Groups Read %09d", collectorReportData.getNumPersistedBatches()); collectorReportWriterService.writeFormattedMessageLine(" Total Groups Bypassed %09d", collectorReportData.getNumNotPersistedBatches()); int totalRecordsBypassed = collectorReportData.getNumNotPersistedOriginEntryRecords() + collectorReportData.getNumNotPersistedCollectorDetailRecords(); collectorReportWriterService.writeFormattedMessageLine(" Total Records Bypassed %09d", totalRecordsBypassed); collectorReportWriterService.writeFormattedMessageLine(" Total WWW Records Out %09d", aggregateNumInputDetails); int aggregateOriginEntryCountFromParsedData = aggregateOriginEntryTotals.getNumCreditEntries() + aggregateOriginEntryTotals.getNumDebitEntries() + aggregateOriginEntryTotals.getNumOtherEntries(); collectorReportWriterService.writeFormattedMessageLine(" Total GLE Records Out %09d", aggregateOriginEntryCountFromParsedData); collectorReportWriterService.writeFormattedMessageLine(" Total GLE Debits %19s", new KualiDecimalFormatter(aggregateOriginEntryTotals.getDebitAmount())); collectorReportWriterService.writeFormattedMessageLine(" Debit Count %09d", aggregateOriginEntryTotals.getNumDebitEntries()); collectorReportWriterService.writeFormattedMessageLine(" Total GLE Credits %19s", new KualiDecimalFormatter(aggregateOriginEntryTotals.getCreditAmount())); collectorReportWriterService.writeFormattedMessageLine(" Credit Count %09d", aggregateOriginEntryTotals.getNumCreditEntries()); collectorReportWriterService.writeFormattedMessageLine(" Total GLE Not C or D %19s", new KualiDecimalFormatter(aggregateOriginEntryTotals.getOtherAmount())); collectorReportWriterService.writeFormattedMessageLine(" Not C or D Count %09d", aggregateOriginEntryTotals.getNumOtherEntries()); collectorReportWriterService.writeNewLines(1); collectorReportWriterService.writeFormattedMessageLine("Inserted %d detail records into gl_id_bill_t", aggregateNumSavedDetails); } /** * Appends header information to the given buffer * * @param buf the buffer where the message should go * @param batch the data from the Collector file */ protected void appendHeaderInformation(StringBuilder buf, CollectorBatch batch, CollectorReportData collectorReportData) { String emailBatchStatus = collectorReportData.getEmailSendingStatus().get(batch.getBatchName()); buf.append("\n Chart: ").append(batch.getChartOfAccountsCode()).append("\n"); buf.append(" Org: ").append(batch.getOrganizationCode()).append("\n"); buf.append(" Campus: ").append(batch.getCampusCode()).append("\n"); buf.append(" Department: ").append(batch.getDepartmentName()).append("\n"); buf.append(" Mailing Address: ").append(batch.getMailingAddress()).append("\n"); buf.append(" Contact: ").append(batch.getPersonUserID()).append("\n"); buf.append(" Email: ").append(batch.getEmailAddress()); if (StringUtils.isNotEmpty(emailBatchStatus)){ String displayStatus = StringUtils.containsIgnoreCase(emailBatchStatus, "ERROR")? "**Email Failure" : "**Email Success"; buf.append(" ( " + displayStatus + " )").append("\n"); }else{ buf.append("\n"); } buf.append(" Transmission Date: ").append(batch.getTransmissionDate()).append("\n\n"); } /** * Writes totals information to the report * * @param buf the buffer where the e-mail report is being written * @param batch the data generated by the Collector file upload * @param totals the totals to write */ protected void appendTotalsInformation(StringBuilder buf, CollectorBatch batch) { OriginEntryTotals totals = batch.getOriginEntryTotals(); if (totals == null) { buf.append(" Totals are unavailable for this batch.\n"); } else { // SUMMARY TOTALS HERE appendAmountCountLine(buf, "Group Credits = ", Integer.toString(totals.getNumCreditEntries()), totals.getCreditAmount()); appendAmountCountLine(buf, "Group Debits = ", Integer.toString(totals.getNumDebitEntries()), totals.getDebitAmount()); appendAmountCountLine(buf, "Group Not C/D = ", Integer.toString(totals.getNumOtherEntries()), totals.getOtherAmount()); appendAmountCountLine(buf, "Valid Group Count = ", batch.getTotalRecords().toString(), batch.getTotalAmount()); } } /** * Writes the Amount/Count line of the Collector to a buffer * * @param buf the buffer to write the line to * @param countTitle the title of this part of the report * @param count the Collector count * @param amountString the Collector amount */ protected void appendAmountCountLine(StringBuilder buf, String countTitle, String count, KualiDecimal amount) { appendPaddingString(buf, ' ', countTitle.length(), 35); buf.append(countTitle); appendPaddingString(buf, '0', count.length(), 5); buf.append(count); if (amount == null) { buf.append(StringUtils.leftPad("N/A", 21)); } else { Map<String, String> settings = new HashMap<String, String>(); settings.put(CurrencyFormatter.SHOW_SYMBOL, Boolean.TRUE.toString()); org.kuali.rice.core.web.format.Formatter f = org.kuali.rice.core.web.format.Formatter.getFormatter(KualiDecimal.class, settings); String amountString = (String) f.format(amount); appendPaddingString(buf, ' ', amountString.length(), 21); buf.append(amountString); } buf.append("\n"); } /** * Writes some padding to a buffer * * @param buf the buffer to write to * @param padCharacter the character to repeat in the pad * @param valueLength the length of the value being padded * @param desiredLength the length the whole String should be * @return the buffer */ protected StringBuilder appendPaddingString(StringBuilder buf, char padCharacter, int valueLength, int desiredLength) { for (int i = valueLength; i < desiredLength; i++) { buf.append(padCharacter); } return buf; } protected void appendPreScrubberReport(CollectorReportData collectorReportData) { if (preScrubberService.deriveChartOfAccountsCodeIfSpaces()) { collectorReportWriterService.pageBreak(); collectorReportWriterService.writeSubTitle("Collector Pre-Scrubber Report"); new PreScrubberReport().generateReport(collectorReportData.getPreScrubberReportData(), collectorReportWriterService); } } /** * Writes the results of the Scrubber's run on the Collector data to the report writer * * @param collectorReportData data gathered from the run of the Collector */ protected void appendScrubberReport(CollectorReportData collectorReportData) { Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches(); ScrubberReportData aggregateScrubberReportData = new ScrubberReportData(); Map<Transaction, List<Message>> aggregateScrubberErrors = new LinkedHashMap<Transaction, List<Message>>(); collectorReportWriterService.pageBreak(); while (batchIter.hasNext()) { CollectorBatch batch = batchIter.next(); ScrubberReportData batchScrubberReportData = collectorReportData.getScrubberReportData(batch); if (batchScrubberReportData != null) { // if some validation error occurred during batch load, the scrubber wouldn't have been run, so there'd be no data aggregateScrubberReportData.incorporateReportData(batchScrubberReportData); } Map<Transaction, List<Message>> batchScrubberReportErrors = collectorReportData.getBatchOriginEntryScrubberErrors(batch); if (batchScrubberReportErrors != null) { // if some validation error occurred during batch load, the scrubber wouldn't have been run, so there'd be a null map aggregateScrubberErrors.putAll(batchScrubberReportErrors); } } List<Transaction> transactions = new ArrayList<Transaction>(aggregateScrubberErrors.keySet()); for (Transaction errorTrans : aggregateScrubberErrors.keySet()) { List<Message> errors = aggregateScrubberErrors.get(errorTrans); collectorReportWriterService.writeError(errorTrans, errors); } collectorReportWriterService.writeStatisticLine("UNSCRUBBED RECORDS READ %,9d", aggregateScrubberReportData.getNumberOfUnscrubbedRecordsRead()); collectorReportWriterService.writeStatisticLine("SCRUBBED RECORDS WRITTEN %,9d", aggregateScrubberReportData.getNumberOfScrubbedRecordsWritten()); collectorReportWriterService.writeStatisticLine("ERROR RECORDS WRITTEN %,9d", aggregateScrubberReportData.getNumberOfErrorRecordsWritten()); collectorReportWriterService.writeStatisticLine("TOTAL OUTPUT RECORDS WRITTEN %,9d", aggregateScrubberReportData.getTotalNumberOfRecordsWritten()); collectorReportWriterService.writeStatisticLine("EXPIRED ACCOUNTS FOUND %,9d", aggregateScrubberReportData.getNumberOfExpiredAccountsFound()); } /** * Writes the report of the demerger run against the Collector data * * @param collectorReportData data gathered from the run of the Collector * @throws DocumentException the exception thrown if the PDF cannot be written to */ protected void appendDemergerReport(CollectorReportData collectorReportData) { Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches(); DemergerReportData aggregateDemergerReportData = new DemergerReportData(); ScrubberReportData aggregateScrubberReportData = new ScrubberReportData(); while (batchIter.hasNext()) { CollectorBatch batch = batchIter.next(); DemergerReportData batchDemergerReportData = collectorReportData.getDemergerReportData(batch); if (batchDemergerReportData != null) { aggregateDemergerReportData.incorporateReportData(batchDemergerReportData); } } collectorReportWriterService.pageBreak(); collectorReportWriterService.writeStatisticLine("ERROR RECORDS READ %,9d", aggregateDemergerReportData.getErrorTransactionsRead()); collectorReportWriterService.writeStatisticLine("VALID RECORDS READ %,9d", aggregateDemergerReportData.getValidTransactionsRead()); collectorReportWriterService.writeStatisticLine("ERROR RECORDS REMOVED FROM PROCESSING %,9d", aggregateDemergerReportData.getErrorTransactionsSaved()); collectorReportWriterService.writeStatisticLine("VALID RECORDS ENTERED INTO ORIGIN ENTRY %,9d", aggregateDemergerReportData.getValidTransactionsSaved()); } /** * Writes information about origin entry and details to the report * * @param collectorReportData data gathered from the run of the Collector * @throws DocumentException the exception thrown if the PDF cannot be written to */ protected void appendDeletedOriginEntryAndDetailReport(CollectorReportData collectorReportData) { // figure out how many billing details were removed/bypassed in all of the batches Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches(); int aggregateNumDetailsDeleted = 0; StringBuilder buf = new StringBuilder(); collectorReportWriterService.pageBreak(); collectorReportWriterService.writeFormattedMessageLine("ID-Billing detail data matched with GLE errors to remove documents with errors"); while (batchIter.hasNext()) { CollectorBatch batch = batchIter.next(); Integer batchNumDetailsDeleted = collectorReportData.getNumDetailDeleted(batch); if (batchNumDetailsDeleted != null) { aggregateNumDetailsDeleted += batchNumDetailsDeleted.intValue(); } } collectorReportWriterService.writeFormattedMessageLine("Total-Recs-Bypassed %d", aggregateNumDetailsDeleted); batchIter = collectorReportData.getAddedBatches(); int aggregateTransactionCount = 0; KualiDecimal aggregateDebitAmount = KualiDecimal.ZERO; while (batchIter.hasNext()) { CollectorBatch batch = batchIter.next(); Map<DocumentGroupData, OriginEntryTotals> inputEntryTotals = collectorReportData.getTotalsOnInputOriginEntriesAssociatedWithErrorGroup(batch); if (inputEntryTotals != null) { for (Map.Entry<DocumentGroupData, OriginEntryTotals> errorDocumentGroupEntry : inputEntryTotals.entrySet()) { // normally, blank credit/debit code is treated as a debit, but the ID billing program (the predecessor to the // collector) // was specific about treating only a code of 'D' as a debit collectorReportWriterService.writeFormattedMessageLine("Message sent to %-40s for Document %s", batch.getEmailAddress(), errorDocumentGroupEntry.getKey().getDocumentNumber()); int documentTransactionCount = errorDocumentGroupEntry.getValue().getNumCreditEntries() + errorDocumentGroupEntry.getValue().getNumDebitEntries() + errorDocumentGroupEntry.getValue().getNumOtherEntries(); aggregateTransactionCount += documentTransactionCount; aggregateDebitAmount = aggregateDebitAmount.add(errorDocumentGroupEntry.getValue().getDebitAmount()); collectorReportWriterService.writeFormattedMessageLine("Total Transactions %d for Total Debit Amount %s", documentTransactionCount, new KualiDecimalFormatter(errorDocumentGroupEntry.getValue().getDebitAmount())); } } } collectorReportWriterService.writeFormattedMessageLine("Total Error Records %d", aggregateTransactionCount); collectorReportWriterService.writeFormattedMessageLine("Total Debit Dollars %s", new KualiDecimalFormatter(aggregateDebitAmount)); } /** * Writes information about what details where changed in the Collector to the report * * @param collectorReportData data gathered from the run of the Collector * @throws DocumentException the exception thrown if the PDF cannot be written to */ protected void appendDetailChangedAccountReport(CollectorReportData collectorReportData) { StringBuilder buf = new StringBuilder(); collectorReportWriterService.writeNewLines(3); collectorReportWriterService.writeFormattedMessageLine("ID-Billing Detail Records with Account Numbers Changed Due to Change of Corresponding GLE Data"); Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches(); int aggregateNumDetailAccountValuesChanged = 0; while (batchIter.hasNext()) { CollectorBatch batch = batchIter.next(); Integer batchNumDetailAccountValuesChanged = collectorReportData.getNumDetailAccountValuesChanged(batch); if (batchNumDetailAccountValuesChanged != null) { aggregateNumDetailAccountValuesChanged += batchNumDetailAccountValuesChanged; } } collectorReportWriterService.writeFormattedMessageLine("Tot-Recs-Changed %d", aggregateNumDetailAccountValuesChanged); } /** * Sets the dateTimeService attribute value. * * @param dateTimeService The dateTimeService to set. */ public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } /** * Generate the header for the demerger status report. * * @param scrubberReportData the data gathered from the run of the scrubber on the collector data * @param demergerReport the data gathered from the run of the demerger on the collector data * @return list of report summaries to be printed */ protected List<Summary> buildDemergerReportSummary(ScrubberReportData scrubberReportData, DemergerReportData demergerReport) { List<Summary> reportSummary = new ArrayList<Summary>(); reportSummary.add(new Summary(1, "ERROR RECORDS READ", new Integer(scrubberReportData.getNumberOfErrorRecordsWritten()))); reportSummary.add(new Summary(2, "VALID RECORDS READ", new Integer(scrubberReportData.getNumberOfScrubbedRecordsWritten()))); reportSummary.add(new Summary(3, "ERROR RECORDS REMOVED FROM PROCESSING", new Integer(demergerReport.getErrorTransactionsSaved()))); reportSummary.add(new Summary(4, "VALID RECORDS ENTERED INTO ORIGIN ENTRY", new Integer(demergerReport.getValidTransactionsSaved()))); return reportSummary; } /** * Adds the ledger report to this Collector report * * @param collectorReportData the data from the Collector run * @throws DocumentException thrown if it is impossible to write to the report */ protected void appendLedgerReport(CollectorReportData collectorReportData) { collectorReportWriterService.pageBreak(); collectorReportWriterService.writeSubTitle("GENERAL LEDGER INPUT TRANSACTIONS FROM COLLECTOR"); collectorReportWriterService.writeNewLines(1); LedgerSummaryReport ledgerSummaryReport = collectorReportData.getLedgerSummaryReport(); ledgerSummaryReport.writeReport(collectorReportWriterService); } /** * Builds actual error message from error key and parameters. * @param errorMap a map of errors * @return List<String> of error message text */ protected List<String> translateErrorsFromMessageMap(MessageMap messageMap) { List<String> collectorErrors = new ArrayList<String>(); for (Iterator<String> iter = messageMap.getPropertiesWithErrors().iterator(); iter.hasNext();) { String errorKey = iter.next(); for (Iterator<ErrorMessage> iter2 = messageMap.getMessages(errorKey).iterator(); iter2.hasNext();) { ErrorMessage errorMessage = (ErrorMessage) iter2.next(); String messageText = configurationService.getPropertyValueAsString(errorMessage.getErrorKey()); collectorErrors.add(MessageFormat.format(messageText, (Object[]) errorMessage.getMessageParameters())); } } return collectorErrors; } /** * Sends email with results of the batch processing. * @param batch the Collector data from the file * @param collectorReportData data gathered from the run of the Collector */ protected void sendValidationEmail(CollectorBatch batch, CollectorReportData collectorReportData) { if (StringUtils.isBlank(batch.getEmailAddress())) { LOG.error("Email not sent because email is blank, batch name " + batch.getBatchName()); return; } MessageMap messageMap = batch.getMessageMap(); List<String> errorMessages = translateErrorsFromMessageMap(messageMap); LOG.debug("sendValidationEmail() starting"); MailMessage message = new MailMessage(); String returnAddress = parameterService.getParameterValueAsString(KFSConstants.ParameterNamespaces.GL, "Batch", KFSConstants.FROM_EMAIL_ADDRESS_PARM_NM); if(StringUtils.isEmpty(returnAddress)) { returnAddress = mailService.getBatchMailingList(); } message.setFromAddress(returnAddress); String subject = parameterService.getParameterValueAsString(CollectorStep.class, SystemGroupParameterNames.COLLECTOR_VALIDATOR_EMAIL_SUBJECT_PARAMETER_NAME); //KFSMI-5918 if (errorMessages.size() >0){ subject = parameterService.getParameterValueAsString(CollectorStep.class, SystemGroupParameterNames.COLLECTOR_VALIDATOR_ERROR_EMAIL_SUBJECT_PARAMETER_NAME); } message.setSubject(subject); String body = createValidationMessageBody(errorMessages, batch, collectorReportData); message.setMessage(body); message.addToAddress(batch.getEmailAddress()); try { mailService.sendMessage(message); String notificationMessage = configurationService.getPropertyValueAsString(KFSKeyConstants.Collector.NOTIFICATION_EMAIL_SENT); String formattedMessage = MessageFormat.format(notificationMessage, new Object[] { batch.getEmailAddress() }); collectorReportData.setEmailSendingStatusForParsedBatch(batch, formattedMessage); } catch (Exception e) { LOG.error("sendErrorEmail() Invalid email address. Message not sent", e); String errorMessage = configurationService.getPropertyValueAsString(KFSKeyConstants.Collector.EMAIL_SEND_ERROR); String formattedMessage = MessageFormat.format(errorMessage, new Object[] { batch.getEmailAddress() }); collectorReportData.setEmailSendingStatusForParsedBatch(batch, formattedMessage); } } /** * Sends the e-mail about the demerger step * * @param batch the data from the Collector file * @param collectorReportData data gathered from the run of the Collector */ protected void sendDemergerEmail(CollectorBatch batch, CollectorReportData collectorReportData) { if (StringUtils.isBlank(batch.getEmailAddress())) { LOG.error("Email not sent because email is blank, batch name " + batch.getBatchName()); return; } LOG.debug("sendDemergerEmail() starting"); String body = createDemergerMessageBody(batch, collectorReportData); if (body == null) { // there must not have been anything to send, so just return from this method return; } MailMessage message = new MailMessage(); String returnAddress = parameterService.getParameterValueAsString(KFSConstants.ParameterNamespaces.GL, "Batch", KFSConstants.FROM_EMAIL_ADDRESS_PARM_NM); if(StringUtils.isEmpty(returnAddress)) { returnAddress = mailService.getBatchMailingList(); } message.setFromAddress(returnAddress); String subject = parameterService.getParameterValueAsString(CollectorStep.class, SystemGroupParameterNames.COLLECTOR_DEMERGER_EMAIL_SUBJECT_PARAMETER_NAME); message.setSubject(subject); message.setMessage(body); message.addToAddress(batch.getEmailAddress()); try { mailService.sendMessage(message); String notificationMessage = configurationService.getPropertyValueAsString(KFSKeyConstants.Collector.NOTIFICATION_EMAIL_SENT); String formattedMessage = MessageFormat.format(notificationMessage, new Object[] { batch.getEmailAddress() }); collectorReportData.setEmailSendingStatusForParsedBatch(batch, formattedMessage); } catch (Exception e) { LOG.error("sendErrorEmail() Invalid email address. Message not sent", e); String errorMessage = configurationService.getPropertyValueAsString(KFSKeyConstants.Collector.EMAIL_SEND_ERROR); String formattedMessage = MessageFormat.format(errorMessage, new Object[] { batch.getEmailAddress() }); collectorReportData.setEmailSendingStatusForParsedBatch(batch, formattedMessage); } } /** * Sends email message to batch mailing list notifying of email send failures during the collector processing * * @param collectorReportData - data from collector run */ protected void sendEmailSendFailureNotice(CollectorReportData collectorReportData) { MailMessage message = new MailMessage(); String returnAddress = parameterService.getParameterValueAsString(KFSConstants.ParameterNamespaces.GL, "Batch", KFSConstants.FROM_EMAIL_ADDRESS_PARM_NM); if(StringUtils.isEmpty(returnAddress)) { returnAddress = mailService.getBatchMailingList(); } message.setFromAddress(returnAddress); String subject = configurationService.getPropertyValueAsString(KFSKeyConstants.ERROR_COLLECTOR_EMAILSEND_NOTIFICATION_SUBJECT); message.setSubject(subject); boolean hasEmailSendErrors = false; String body = configurationService.getPropertyValueAsString(KFSKeyConstants.ERROR_COLLECTOR_EMAILSEND_NOTIFICATION_BODY); for (String batchId : collectorReportData.getEmailSendingStatus().keySet()) { String emailStatus = collectorReportData.getEmailSendingStatus().get(batchId); if (StringUtils.containsIgnoreCase(emailStatus, "error")) { body += "Batch: " + batchId + " - " + emailStatus + "\n"; hasEmailSendErrors = true; } } message.setMessage(body); message.addToAddress(mailService.getBatchMailingList()); try { if (hasEmailSendErrors) { mailService.sendMessage(message); LOG.info("Email failure notice has been sent to : " + message.getToAddresses() ); } } catch (Exception e) { LOG.error("sendErrorEmail() Invalid email address. Message not sent", e); } } /** * Creates a section about validation messages * * @param errorMessages a List of errors that happened during the Collector run * @param batch the data from the Collector file * @param collectorReportData data gathered from the run of the Collector * @return the Validation message body */ protected String createValidationMessageBody(List<String> errorMessages, CollectorBatch batch, CollectorReportData collectorReportData) { StringBuilder body = new StringBuilder(); MessageMap fileMessageMap = batch.getMessageMap(); body.append("Header Information:\n\n"); if (!fileMessageMap.containsMessageKey(KFSKeyConstants.ERROR_BATCH_UPLOAD_PARSING_XML)) { appendHeaderInformation(body, batch, collectorReportData); appendTotalsInformation(body, batch); appendValidationStatus(body, errorMessages, true, 0); } return body.toString(); } /** * Generates a String that reports on the validation status of the document * * @param errorMessages a List of error messages encountered in the Collector process * @param notifyIfSuccessful true if a special message for the process running successfully should be added, false otherwise * @param numLeftPaddingSpaces the number of spaces to pad on the left * @return a String with the validation status message */ protected String getValidationStatus(List<String> errorMessages, boolean notifyIfSuccessful, int numLeftPaddingSpaces) { StringBuilder buf = new StringBuilder(); appendValidationStatus(buf, errorMessages, notifyIfSuccessful, numLeftPaddingSpaces); return buf.toString(); } /** * Appends the validation status message to a buffer * * @param buf a StringBuilder to append error messages to * @param errorMessages a List of error messages encountered in the Collector process * @param notifyIfSuccessful true if a special message for the process running successfully should be added, false otherwise * @param numLeftPaddingSpaces the number of spaces to pad on the left */ protected void appendValidationStatus(StringBuilder buf, List<String> errorMessages, boolean notifyIfSuccessful, int numLeftPaddingSpaces) { String padding = StringUtils.leftPad("", numLeftPaddingSpaces, ' '); if (notifyIfSuccessful || !errorMessages.isEmpty()) { buf.append("\n").append(padding).append("Reported Errors:\n"); } // ERRORS GO HERE if (errorMessages.isEmpty() && notifyIfSuccessful) { buf.append(padding).append("----- NO ERRORS TO REPORT -----\nThis file will be processed by the accounting cycle.\n"); } else if (!errorMessages.isEmpty()) { for (String currentMessage : errorMessages) { buf.append(padding).append(currentMessage + "\n"); } buf.append("\n").append(padding).append("----- THIS FILE WAS NOT PROCESSED AND WILL NEED TO BE CORRECTED AND RESUBMITTED -----\n"); } } /** * Writes the part of the report about the demerger * * @param batch the data from the Collector file * @param collectorReportData data gathered from the run of the Collector * @return */ protected String createDemergerMessageBody(CollectorBatch batch, CollectorReportData collectorReportData) { StringBuilder buf = new StringBuilder(); appendHeaderInformation(buf, batch, collectorReportData); Map<Transaction, List<Message>> batchOriginEntryScrubberErrors = collectorReportData.getBatchOriginEntryScrubberErrors(batch); // the keys of the map returned by getTotalsOnInputOriginEntriesAssociatedWithErrorGroup represent all of the error document // groups in the system Map<DocumentGroupData, OriginEntryTotals> errorGroupDocumentTotals = collectorReportData.getTotalsOnInputOriginEntriesAssociatedWithErrorGroup(batch); Set<DocumentGroupData> errorDocumentGroups = null; if (errorGroupDocumentTotals == null) { return null; } errorDocumentGroups = errorGroupDocumentTotals.keySet(); if (errorDocumentGroups.isEmpty()) { return null; } else { for (DocumentGroupData errorDocumentGroup : errorDocumentGroups) { buf.append("Document ").append(errorDocumentGroup.getDocumentNumber()).append(" Rejected Due to Editing Errors.\n"); for (Transaction transaction : batchOriginEntryScrubberErrors.keySet()) { if (errorDocumentGroup.matchesTransaction(transaction)) { if (transaction instanceof OriginEntryFull) { OriginEntryFull entry = (OriginEntryFull) transaction; buf.append(" Origin Entry: ").append(entry.getLine()).append("\n"); for (Message message : batchOriginEntryScrubberErrors.get(transaction)) { buf.append(" ").append(message.getMessage()).append("\n"); } } } } } } return buf.toString(); } public void setMailService(MailService mailService) { this.mailService = mailService; } public void setConfigurationService(ConfigurationService configurationService) { this.configurationService = configurationService; } public void setParameterService(ParameterService parameterService) { this.parameterService = parameterService; } /** * Sets the collectorReportWriterService attribute value. * @param collectorReportWriterService The collectorReportWriterService to set. */ public void setCollectorReportWriterService(ReportWriterService collectorReportWriterService) { this.collectorReportWriterService = collectorReportWriterService; } public void setPreScrubberService(PreScrubberService preScrubberService) { this.preScrubberService = preScrubberService; } protected static class KualiDecimalFormatter implements Formattable { private KualiDecimal number; public KualiDecimalFormatter(KualiDecimal numberToFormat) { this.number = numberToFormat; } @Override public void formatTo(Formatter formatter, int flags, int width, int precision) { Map<String, String> settings = new HashMap<String, String>(); settings.put(CurrencyFormatter.SHOW_SYMBOL, Boolean.TRUE.toString()); org.kuali.rice.core.web.format.Formatter cf = org.kuali.rice.core.web.format.Formatter.getFormatter(KualiDecimal.class, settings); formatter.format((String) cf.format(number)); } } }