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.service.impl;
017    
018    import java.text.MessageFormat;
019    import java.util.ArrayList;
020    import java.util.Formattable;
021    import java.util.Formatter;
022    import java.util.HashMap;
023    import java.util.Iterator;
024    import java.util.LinkedHashMap;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.Set;
028    
029    import org.apache.commons.lang.StringUtils;
030    import org.kuali.kfs.gl.batch.CollectorBatch;
031    import org.kuali.kfs.gl.batch.CollectorStep;
032    import org.kuali.kfs.gl.batch.service.CollectorReportService;
033    import org.kuali.kfs.gl.businessobject.DemergerReportData;
034    import org.kuali.kfs.gl.businessobject.OriginEntryFull;
035    import org.kuali.kfs.gl.businessobject.Transaction;
036    import org.kuali.kfs.gl.report.CollectorReportData;
037    import org.kuali.kfs.gl.report.LedgerSummaryReport;
038    import org.kuali.kfs.gl.report.PreScrubberReport;
039    import org.kuali.kfs.gl.report.Summary;
040    import org.kuali.kfs.gl.service.PreScrubberService;
041    import org.kuali.kfs.gl.service.ScrubberReportData;
042    import org.kuali.kfs.sys.KFSConstants;
043    import org.kuali.kfs.sys.KFSConstants.SystemGroupParameterNames;
044    import org.kuali.kfs.sys.KFSKeyConstants;
045    import org.kuali.kfs.sys.Message;
046    import org.kuali.kfs.sys.service.ReportWriterService;
047    import org.kuali.rice.kns.mail.InvalidAddressException;
048    import org.kuali.rice.kns.mail.MailMessage;
049    import org.kuali.rice.kns.service.DateTimeService;
050    import org.kuali.rice.kns.service.KualiConfigurationService;
051    import org.kuali.rice.kns.service.MailService;
052    import org.kuali.rice.kns.service.ParameterService;
053    import org.kuali.rice.kns.util.ErrorMessage;
054    import org.kuali.rice.kns.util.KualiDecimal;
055    import org.kuali.rice.kns.util.MessageMap;
056    import org.kuali.rice.kns.web.format.CurrencyFormatter;
057    
058    /**
059     * The base implementation of the CollectorReportService
060     */
061    public class CollectorReportServiceImpl implements CollectorReportService {
062        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CollectorReportServiceImpl.class);
063    
064        private DateTimeService dateTimeService;
065        private ParameterService parameterService;
066        private KualiConfigurationService configurationService;
067        private MailService mailService;
068        private PreScrubberService preScrubberService;
069        private ReportWriterService collectorReportWriterService;
070    
071        /**
072         * Constructs a CollectorReportServiceImpl instance
073         */
074        public CollectorReportServiceImpl() {
075        }
076    
077        /**
078         * Sends out e-mails about the validation and demerger of the Collector run
079         * 
080         * @param collectorReportData data gathered from the run of the Collector
081         * @see org.kuali.kfs.gl.batch.service.CollectorReportService#sendEmails(org.kuali.kfs.gl.report.CollectorReportData)
082         */
083        public void sendEmails(CollectorReportData collectorReportData) {
084            // send out the validation status messages
085            Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches();
086            while (batchIter.hasNext()) {
087                CollectorBatch batch = batchIter.next();
088                sendValidationEmail(batch, collectorReportData);
089                sendDemergerEmail(batch, collectorReportData);
090            }
091            
092            sendEmailSendFailureNotice(collectorReportData);
093        }
094    
095        /**
096         * Generates the reports about a given Collector run
097         * 
098         * @param collectorReportData data gathered from the run of the Collector
099         * @see org.kuali.kfs.gl.batch.service.CollectorReportService#generateCollectorRunReports(org.kuali.kfs.gl.report.CollectorReportData)
100         */
101        public void generateCollectorRunReports(CollectorReportData collectorReportData) {
102            appendCollectorHeaderInformation(collectorReportData);
103            appendPreScrubberReport(collectorReportData);
104            appendScrubberReport(collectorReportData);
105            appendDemergerReport(collectorReportData);
106            appendDeletedOriginEntryAndDetailReport(collectorReportData);
107            appendDetailChangedAccountReport(collectorReportData);
108            appendLedgerReport(collectorReportData);
109        }
110    
111        /**
112         * Appends Collector header information to the report writer
113         * 
114         * @param collectorReportData data gathered from the run of the Collector
115         */
116        protected void appendCollectorHeaderInformation(CollectorReportData collectorReportData) {
117            Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches();
118            OriginEntryTotals aggregateOriginEntryTotals = new OriginEntryTotals();
119            int aggregateTotalRecordsCountFromTrailer = 0;
120            int aggregateNumInputDetails = 0;
121            int aggregateNumSavedDetails = 0;
122    
123            if (!collectorReportData.getAllUnparsableFileNames().isEmpty()) {
124                collectorReportWriterService.writeFormattedMessageLine("The following files could not be parsed:\n\n");
125                for (String unparsableFileName : collectorReportData.getAllUnparsableFileNames()) {
126                    List<String> batchErrors = translateErrorsFromErrorMap(collectorReportData.getMessageMapForFileName(unparsableFileName));
127                    collectorReportWriterService.writeFormattedMessageLine("        " + unparsableFileName + "\n");
128                    for (String errorMessage : batchErrors) {
129                        collectorReportWriterService.writeFormattedMessageLine("        - ERROR MESSAGE: " + errorMessage);
130                    }
131                }
132            }
133    
134            while (batchIter.hasNext()) {
135                CollectorBatch batch = batchIter.next();
136                StringBuilder buf = new StringBuilder();
137    
138                OriginEntryTotals batchOriginEntryTotals = batch.getOriginEntryTotals();
139                appendHeaderInformation(buf, batch);
140                appendTotalsInformation(buf, batch);
141    
142                List<String> errorMessages = translateErrorsFromErrorMap(batch.getMessageMap());
143    
144                aggregateTotalRecordsCountFromTrailer += batch.getTotalRecords();
145    
146                // if batch is valid add up totals
147                if (collectorReportData.isBatchValid(batch)) {
148    
149                    if (batchOriginEntryTotals != null) {
150                        aggregateOriginEntryTotals.incorporateTotals(batchOriginEntryTotals);
151                    }
152    
153                    Integer batchNumInputDetails = collectorReportData.getNumInputDetails(batch);
154                    if (batchNumInputDetails != null) {
155                        aggregateNumInputDetails += batchNumInputDetails;
156                    }
157    
158                    Integer batchNumSavedDetails = collectorReportData.getNumSavedDetails(batch);
159                    if (batchNumSavedDetails != null) {
160                        aggregateNumSavedDetails += batchNumSavedDetails;
161                    }
162                }
163    
164                collectorReportWriterService.writeFormattedMessageLine("Header  *********************************************************************");
165                collectorReportWriterService.writeMultipleFormattedMessageLines(buf.toString());
166    
167                String validationErrors = getValidationStatus(errorMessages, false, 15);
168                if (StringUtils.isNotBlank(validationErrors)) {
169                    collectorReportWriterService.writeMultipleFormattedMessageLines(validationErrors);
170                }
171            }
172    
173            collectorReportWriterService.writeNewLines(2);
174            collectorReportWriterService.writeFormattedMessageLine("***** Totals for Creation of GLE Data  *****");
175            collectorReportWriterService.writeFormattedMessageLine("      Total Records Read      %09d", aggregateTotalRecordsCountFromTrailer);
176            collectorReportWriterService.writeFormattedMessageLine("      Total Groups Read       %09d", collectorReportData.getNumPersistedBatches());
177            collectorReportWriterService.writeFormattedMessageLine("      Total Groups Bypassed   %09d", collectorReportData.getNumNotPersistedBatches());
178            int totalRecordsBypassed = collectorReportData.getNumNotPersistedOriginEntryRecords() + collectorReportData.getNumNotPersistedCollectorDetailRecords();
179            collectorReportWriterService.writeFormattedMessageLine("      Total Records Bypassed  %09d", totalRecordsBypassed);
180            collectorReportWriterService.writeFormattedMessageLine("      Total WWW Records Out   %09d", aggregateNumInputDetails);
181            int aggregateOriginEntryCountFromParsedData = aggregateOriginEntryTotals.getNumCreditEntries() + aggregateOriginEntryTotals.getNumDebitEntries() + aggregateOriginEntryTotals.getNumOtherEntries();
182            collectorReportWriterService.writeFormattedMessageLine("      Total GLE Records Out   %09d", aggregateOriginEntryCountFromParsedData);
183            collectorReportWriterService.writeFormattedMessageLine("      Total GLE Debits        %19s", new KualiDecimalFormatter(aggregateOriginEntryTotals.getDebitAmount()));
184            collectorReportWriterService.writeFormattedMessageLine("      Debit Count             %09d", aggregateOriginEntryTotals.getNumDebitEntries());
185            collectorReportWriterService.writeFormattedMessageLine("      Total GLE Credits       %19s", new KualiDecimalFormatter(aggregateOriginEntryTotals.getCreditAmount()));
186            collectorReportWriterService.writeFormattedMessageLine("      Debit Count             %09d", aggregateOriginEntryTotals.getNumCreditEntries());
187            collectorReportWriterService.writeFormattedMessageLine("      Total GLE Not C or D    %19s", new KualiDecimalFormatter(aggregateOriginEntryTotals.getOtherAmount()));
188            collectorReportWriterService.writeFormattedMessageLine("      Not C or D Count        %09d", aggregateOriginEntryTotals.getNumOtherEntries());
189            collectorReportWriterService.writeNewLines(1);
190            collectorReportWriterService.writeFormattedMessageLine("Inserted %d detail records into gl_id_bill_t", aggregateNumSavedDetails);
191        }
192    
193        /**
194         * Appends header information to the given buffer
195         * 
196         * @param buf the buffer where the message should go
197         * @param batch the data from the Collector file
198         */
199        protected void appendHeaderInformation(StringBuilder buf, CollectorBatch batch) {
200            buf.append("\n        Chart: ").append(batch.getChartOfAccountsCode()).append("\n");
201            buf.append("        Org: ").append(batch.getOrganizationCode()).append("\n");
202            buf.append("        Campus: ").append(batch.getCampusCode()).append("\n");
203            buf.append("        Department: ").append(batch.getDepartmentName()).append("\n");
204            buf.append("        Mailing Address: ").append(batch.getMailingAddress()).append("\n");
205            buf.append("        Contact: ").append(batch.getPersonUserID()).append("\n");
206            buf.append("        Email: ").append(batch.getEmailAddress()).append("\n");
207            buf.append("        Transmission Date: ").append(batch.getTransmissionDate()).append("\n\n");
208        }
209    
210        /**
211         * Writes totals information to the report
212         * 
213         * @param buf the buffer where the e-mail report is being written
214         * @param batch the data generated by the Collector file upload
215         * @param totals the totals to write
216         */
217        protected void appendTotalsInformation(StringBuilder buf, CollectorBatch batch) {
218            OriginEntryTotals totals = batch.getOriginEntryTotals();
219            if (totals == null) {
220                buf.append("        Totals are unavailable for this batch.\n");
221            }
222            else {
223                // SUMMARY TOTALS HERE
224                appendAmountCountLine(buf, "Group Credits     = ", Integer.toString(totals.getNumCreditEntries()), totals.getCreditAmount());
225                appendAmountCountLine(buf, "Group Debits      = ", Integer.toString(totals.getNumDebitEntries()), totals.getDebitAmount());
226                appendAmountCountLine(buf, "Group Not C/D     = ", Integer.toString(totals.getNumOtherEntries()), totals.getOtherAmount());
227                appendAmountCountLine(buf, "Valid Group Count = ", batch.getTotalRecords().toString(), batch.getTotalAmount());
228            }
229        }
230    
231        /**
232         * Writes the Amount/Count line of the Collector to a buffer
233         * 
234         * @param buf the buffer to write the line to
235         * @param countTitle the title of this part of the report
236         * @param count the Collector count
237         * @param amountString the Collector amount
238         */
239        protected void appendAmountCountLine(StringBuilder buf, String countTitle, String count, KualiDecimal amount) {
240            appendPaddingString(buf, ' ', countTitle.length(), 35);
241            buf.append(countTitle);
242    
243            appendPaddingString(buf, '0', count.length(), 5);
244            buf.append(count);
245    
246            if (amount == null) {
247                buf.append(StringUtils.leftPad("N/A", 21));
248            }
249            else {
250                Map<String, String> settings = new HashMap<String, String>();
251                settings.put(CurrencyFormatter.SHOW_SYMBOL, Boolean.TRUE.toString());
252                org.kuali.rice.kns.web.format.Formatter f = org.kuali.rice.kns.web.format.Formatter.getFormatter(KualiDecimal.class, settings);
253                String amountString = (String) f.format(amount);
254                appendPaddingString(buf, ' ', amountString.length(), 21);
255                buf.append(amountString);
256            }
257            
258            buf.append("\n");
259    
260        }
261    
262        /**
263         * Writes some padding to a buffer
264         * 
265         * @param buf the buffer to write to
266         * @param padCharacter the character to repeat in the pad
267         * @param valueLength the length of the value being padded
268         * @param desiredLength the length the whole String should be
269         * @return the buffer
270         */
271        protected StringBuilder appendPaddingString(StringBuilder buf, char padCharacter, int valueLength, int desiredLength) {
272            for (int i = valueLength; i < desiredLength; i++) {
273                buf.append(padCharacter);
274            }
275            return buf;
276        }
277    
278        protected void appendPreScrubberReport(CollectorReportData collectorReportData) {
279            if (preScrubberService.deriveChartOfAccountsCodeIfSpaces()) {
280                collectorReportWriterService.pageBreak();
281                collectorReportWriterService.writeSubTitle("Collector Pre-Scrubber Report");
282                new PreScrubberReport().generateReport(collectorReportData.getPreScrubberReportData(), collectorReportWriterService);
283            }
284        }
285        
286        /**
287         * Writes the results of the Scrubber's run on the Collector data to the report writer
288         * 
289         * @param collectorReportData data gathered from the run of the Collector
290         */
291        protected void appendScrubberReport(CollectorReportData collectorReportData) {
292            Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches();
293            ScrubberReportData aggregateScrubberReportData = new ScrubberReportData();
294            Map<Transaction, List<Message>> aggregateScrubberErrors = new LinkedHashMap<Transaction, List<Message>>();
295    
296            collectorReportWriterService.pageBreak();
297            
298            while (batchIter.hasNext()) {
299                CollectorBatch batch = batchIter.next();
300    
301                ScrubberReportData batchScrubberReportData = collectorReportData.getScrubberReportData(batch);
302                if (batchScrubberReportData != null) {
303                    // if some validation error occurred during batch load, the scrubber wouldn't have been run, so there'd be no data
304                    aggregateScrubberReportData.incorporateReportData(batchScrubberReportData);
305                }
306    
307                Map<Transaction, List<Message>> batchScrubberReportErrors = collectorReportData.getBatchOriginEntryScrubberErrors(batch);
308                if (batchScrubberReportErrors != null) {
309                    // if some validation error occurred during batch load, the scrubber wouldn't have been run, so there'd be a null map
310                    aggregateScrubberErrors.putAll(batchScrubberReportErrors);
311                }
312            }
313    
314            List<Transaction> transactions = new ArrayList<Transaction>(aggregateScrubberErrors.keySet());
315            for (Transaction errorTrans : aggregateScrubberErrors.keySet()) {
316                List<Message> errors = aggregateScrubberErrors.get(errorTrans);
317                collectorReportWriterService.writeError(errorTrans, errors);
318            }
319            collectorReportWriterService.writeStatisticLine("UNSCRUBBED RECORDS READ                     %,9d", aggregateScrubberReportData.getNumberOfUnscrubbedRecordsRead());
320            collectorReportWriterService.writeStatisticLine("SCRUBBED RECORDS WRITTEN                    %,9d", aggregateScrubberReportData.getNumberOfScrubbedRecordsWritten());
321            collectorReportWriterService.writeStatisticLine("ERROR RECORDS WRITTEN                       %,9d", aggregateScrubberReportData.getNumberOfErrorRecordsWritten());
322            collectorReportWriterService.writeStatisticLine("TOTAL OUTPUT RECORDS WRITTEN                %,9d", aggregateScrubberReportData.getTotalNumberOfRecordsWritten());
323            collectorReportWriterService.writeStatisticLine("EXPIRED ACCOUNTS FOUND                      %,9d", aggregateScrubberReportData.getNumberOfExpiredAccountsFound());
324        }
325    
326        /**
327         * Writes the report of the demerger run against the Collector data 
328         * 
329         * @param collectorReportData data gathered from the run of the Collector
330         * @throws DocumentException the exception thrown if the PDF cannot be written to
331         */
332        protected void appendDemergerReport(CollectorReportData collectorReportData) {
333            Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches();
334            DemergerReportData aggregateDemergerReportData = new DemergerReportData();
335            ScrubberReportData aggregateScrubberReportData = new ScrubberReportData();
336    
337            while (batchIter.hasNext()) {
338                CollectorBatch batch = batchIter.next();
339                DemergerReportData batchDemergerReportData = collectorReportData.getDemergerReportData(batch);
340                if (batchDemergerReportData != null) {
341                    aggregateDemergerReportData.incorporateReportData(batchDemergerReportData);
342                }
343            }
344    
345            collectorReportWriterService.pageBreak();
346            collectorReportWriterService.writeStatisticLine("ERROR RECORDS READ                          %,9d", aggregateDemergerReportData.getErrorTransactionsRead());
347            collectorReportWriterService.writeStatisticLine("VALID RECORDS READ                          %,9d", aggregateDemergerReportData.getValidTransactionsRead());
348            collectorReportWriterService.writeStatisticLine("ERROR RECORDS REMOVED FROM PROCESSING       %,9d", aggregateDemergerReportData.getErrorTransactionsSaved());
349            collectorReportWriterService.writeStatisticLine("VALID RECORDS ENTERED INTO ORIGIN ENTRY     %,9d", aggregateDemergerReportData.getValidTransactionsSaved());
350        }
351    
352        /**
353         * Writes information about origin entry and details to the report
354         * 
355         * @param collectorReportData data gathered from the run of the Collector
356         * @throws DocumentException the exception thrown if the PDF cannot be written to
357         */
358        protected void appendDeletedOriginEntryAndDetailReport(CollectorReportData collectorReportData) {
359            // figure out how many billing details were removed/bypassed in all of the batches
360            Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches();
361            int aggregateNumDetailsDeleted = 0;
362    
363            StringBuilder buf = new StringBuilder();
364    
365            collectorReportWriterService.pageBreak();
366            collectorReportWriterService.writeFormattedMessageLine("ID-Billing detail data matched with GLE errors to remove documents with errors");
367            while (batchIter.hasNext()) {
368                CollectorBatch batch = batchIter.next();
369    
370                Integer batchNumDetailsDeleted = collectorReportData.getNumDetailDeleted(batch);
371                if (batchNumDetailsDeleted != null) {
372                    aggregateNumDetailsDeleted += batchNumDetailsDeleted.intValue();
373                }
374            }
375            collectorReportWriterService.writeFormattedMessageLine("Total-Recs-Bypassed  %d", aggregateNumDetailsDeleted);
376    
377            batchIter = collectorReportData.getAddedBatches();
378            int aggregateTransactionCount = 0;
379            KualiDecimal aggregateDebitAmount = KualiDecimal.ZERO;
380            while (batchIter.hasNext()) {
381                CollectorBatch batch = batchIter.next();
382    
383                Map<DocumentGroupData, OriginEntryTotals> inputEntryTotals = collectorReportData.getTotalsOnInputOriginEntriesAssociatedWithErrorGroup(batch);
384                if (inputEntryTotals != null) {
385                    for (Map.Entry<DocumentGroupData, OriginEntryTotals> errorDocumentGroupEntry : inputEntryTotals.entrySet()) {
386                        // normally, blank credit/debit code is treated as a debit, but the ID billing program (the predecessor to the
387                        // collector)
388                        // was specific about treating only a code of 'D' as a debit
389    
390                        collectorReportWriterService.writeFormattedMessageLine("Message sent to %-40s for Document %s", batch.getEmailAddress(), errorDocumentGroupEntry.getKey().getDocumentNumber());
391                        int documentTransactionCount = errorDocumentGroupEntry.getValue().getNumCreditEntries() + errorDocumentGroupEntry.getValue().getNumDebitEntries() + errorDocumentGroupEntry.getValue().getNumOtherEntries();
392                        aggregateTransactionCount += documentTransactionCount;
393                        aggregateDebitAmount = aggregateDebitAmount.add(errorDocumentGroupEntry.getValue().getDebitAmount());
394                        collectorReportWriterService.writeFormattedMessageLine("Total Transactions %d for Total Debit Amount %s", documentTransactionCount, new KualiDecimalFormatter(errorDocumentGroupEntry.getValue().getDebitAmount()));
395                    }
396                }
397            }
398            collectorReportWriterService.writeFormattedMessageLine("Total Error Records %d", aggregateTransactionCount);
399            collectorReportWriterService.writeFormattedMessageLine("Total Debit Dollars %s", new KualiDecimalFormatter(aggregateDebitAmount));
400        }
401    
402        /**
403         * Writes information about what details where changed in the Collector to the report
404         * 
405         * @param collectorReportData data gathered from the run of the Collector
406         * @throws DocumentException the exception thrown if the PDF cannot be written to
407         */
408        protected void appendDetailChangedAccountReport(CollectorReportData collectorReportData) {
409            StringBuilder buf = new StringBuilder();
410    
411            collectorReportWriterService.writeNewLines(3);
412            collectorReportWriterService.writeFormattedMessageLine("ID-Billing Detail Records with Account Numbers Changed Due to Change of Corresponding GLE Data");
413            Iterator<CollectorBatch> batchIter = collectorReportData.getAddedBatches();
414            int aggregateNumDetailAccountValuesChanged = 0;
415            while (batchIter.hasNext()) {
416                CollectorBatch batch = batchIter.next();
417    
418                Integer batchNumDetailAccountValuesChanged = collectorReportData.getNumDetailAccountValuesChanged(batch);
419                if (batchNumDetailAccountValuesChanged != null) {
420                    aggregateNumDetailAccountValuesChanged += batchNumDetailAccountValuesChanged;
421                }
422            }
423            collectorReportWriterService.writeFormattedMessageLine("Tot-Recs-Changed %d", aggregateNumDetailAccountValuesChanged);
424        }
425    
426        /**
427         * Gets the dateTimeService attribute.
428         * 
429         * @return Returns the dateTimeService.
430         */
431        protected DateTimeService getDateTimeService() {
432            return dateTimeService;
433        }
434    
435        /**
436         * Sets the dateTimeService attribute value.
437         * 
438         * @param dateTimeService The dateTimeService to set.
439         */
440        public void setDateTimeService(DateTimeService dateTimeService) {
441            this.dateTimeService = dateTimeService;
442        }
443    
444        /**
445         * Generate the header for the demerger status report.
446         * 
447         * @param scrubberReportData the data gathered from the run of the scrubber on the collector data
448         * @param demergerReport the data gathered from the run of the demerger on the collector data
449         * @return list of report summaries to be printed
450         */
451        protected List<Summary> buildDemergerReportSummary(ScrubberReportData scrubberReportData, DemergerReportData demergerReport) {
452            List<Summary> reportSummary = new ArrayList<Summary>();
453            reportSummary.add(new Summary(1, "ERROR RECORDS READ", new Integer(scrubberReportData.getNumberOfErrorRecordsWritten())));
454            reportSummary.add(new Summary(2, "VALID RECORDS READ", new Integer(scrubberReportData.getNumberOfScrubbedRecordsWritten())));
455            reportSummary.add(new Summary(3, "ERROR RECORDS REMOVED FROM PROCESSING", new Integer(demergerReport.getErrorTransactionsSaved())));
456            reportSummary.add(new Summary(4, "VALID RECORDS ENTERED INTO ORIGIN ENTRY", new Integer(demergerReport.getValidTransactionsSaved())));
457    
458            return reportSummary;
459        }
460    
461        /**
462         * Adds the ledger report to this Collector report
463         * 
464         * @param collectorReportData the data from the Collector run
465         * @throws DocumentException thrown if it is impossible to write to the report
466         */
467        protected void appendLedgerReport(CollectorReportData collectorReportData) {
468            collectorReportWriterService.pageBreak();
469            collectorReportWriterService.writeSubTitle("GENERAL LEDGER INPUT TRANSACTIONS FROM COLLECTOR");
470            collectorReportWriterService.writeNewLines(1);
471    
472            LedgerSummaryReport ledgerSummaryReport = collectorReportData.getLedgerSummaryReport();
473            ledgerSummaryReport.writeReport(collectorReportWriterService);
474        }
475    
476        /**
477         * Builds actual error message from error key and parameters.
478         * @param errorMap a map of errors
479         * @return List<String> of error message text
480         */
481        protected List<String> translateErrorsFromErrorMap(MessageMap errorMap) {
482            List<String> collectorErrors = new ArrayList<String>();
483    
484            for (Iterator<String> iter = errorMap.getPropertiesWithErrors().iterator(); iter.hasNext();) {
485                String errorKey = iter.next();
486    
487                for (Iterator<ErrorMessage> iter2 = errorMap.getMessages(errorKey).iterator(); iter2.hasNext();) {
488                    ErrorMessage errorMessage = (ErrorMessage) iter2.next();
489                    String messageText = configurationService.getPropertyString(errorMessage.getErrorKey());
490                    collectorErrors.add(MessageFormat.format(messageText, (Object[]) errorMessage.getMessageParameters()));
491                }
492            }
493    
494            return collectorErrors;
495        }
496    
497        /**
498         * Sends email with results of the batch processing.
499         * @param batch the Collector data from the file
500         * @param collectorReportData data gathered from the run of the Collector
501         */
502        protected void sendValidationEmail(CollectorBatch batch, CollectorReportData collectorReportData) {
503            if (StringUtils.isBlank(batch.getEmailAddress())) {
504                LOG.error("Email not sent because email is blank, batch name " + batch.getBatchName());
505                return;
506            }
507            MessageMap errorMap = batch.getMessageMap();
508            List<String> errorMessages = translateErrorsFromErrorMap(errorMap);
509    
510            LOG.debug("sendValidationEmail() starting");
511            MailMessage message = new MailMessage();
512    
513            message.setFromAddress(mailService.getBatchMailingList());
514    
515            String subject = parameterService.getParameterValue(CollectorStep.class, SystemGroupParameterNames.COLLECTOR_VALIDATOR_EMAIL_SUBJECT_PARAMETER_NAME);
516            String productionEnvironmentCode = configurationService.getPropertyString(KFSConstants.PROD_ENVIRONMENT_CODE_KEY);
517            String environmentCode = configurationService.getPropertyString(KFSConstants.ENVIRONMENT_KEY);
518            if (!StringUtils.equals(productionEnvironmentCode, environmentCode)) {
519                subject = environmentCode + ": " + subject;
520            }
521            message.setSubject(subject);
522    
523            String body = createValidationMessageBody(errorMessages, batch, collectorReportData);
524            message.setMessage(body);
525            message.addToAddress(batch.getEmailAddress());
526    
527            try {
528                mailService.sendMessage(message);
529    
530                String notificationMessage = configurationService.getPropertyString(KFSKeyConstants.Collector.NOTIFICATION_EMAIL_SENT);
531                String formattedMessage = MessageFormat.format(notificationMessage, new Object[] { batch.getEmailAddress() });
532                collectorReportData.setEmailSendingStatusForParsedBatch(batch, formattedMessage);
533            }
534            catch (InvalidAddressException e) {
535                LOG.error("sendErrorEmail() Invalid email address. Message not sent", e);
536                String errorMessage = configurationService.getPropertyString(KFSKeyConstants.Collector.EMAIL_SEND_ERROR);
537                String formattedMessage = MessageFormat.format(errorMessage, new Object[] { batch.getEmailAddress() });
538                collectorReportData.setEmailSendingStatusForParsedBatch(batch, formattedMessage);
539            }
540        }
541    
542        /**
543         * Sends the e-mail about the demerger step
544         * 
545         * @param batch the data from the Collector file
546         * @param collectorReportData data gathered from the run of the Collector
547         */
548        protected void sendDemergerEmail(CollectorBatch batch, CollectorReportData collectorReportData) {
549            if (StringUtils.isBlank(batch.getEmailAddress())) {
550                LOG.error("Email not sent because email is blank, batch name " + batch.getBatchName());
551                return;
552            }
553            LOG.debug("sendDemergerEmail() starting");
554            String body = createDemergerMessageBody(batch, collectorReportData);
555            if (body == null) {
556                // there must not have been anything to send, so just return from this method
557                return;
558            }
559            MailMessage message = new MailMessage();
560    
561            message.setFromAddress(mailService.getBatchMailingList());
562    
563            String subject = parameterService.getParameterValue(CollectorStep.class, SystemGroupParameterNames.COLLECTOR_DEMERGER_EMAIL_SUBJECT_PARAMETER_NAME);
564            String productionEnvironmentCode = configurationService.getPropertyString(KFSConstants.PROD_ENVIRONMENT_CODE_KEY);
565            String environmentCode = configurationService.getPropertyString(KFSConstants.ENVIRONMENT_KEY);
566            if (!StringUtils.equals(productionEnvironmentCode, environmentCode)) {
567                subject = environmentCode + ": " + subject;
568            }
569            message.setSubject(subject);
570    
571            message.setMessage(body);
572            message.addToAddress(batch.getEmailAddress());
573    
574            try {
575                mailService.sendMessage(message);
576    
577                String notificationMessage = configurationService.getPropertyString(KFSKeyConstants.Collector.NOTIFICATION_EMAIL_SENT);
578                String formattedMessage = MessageFormat.format(notificationMessage, new Object[] { batch.getEmailAddress() });
579                collectorReportData.setEmailSendingStatusForParsedBatch(batch, formattedMessage);
580            }
581            catch (InvalidAddressException e) {
582                LOG.error("sendErrorEmail() Invalid email address. Message not sent", e);
583                String errorMessage = configurationService.getPropertyString(KFSKeyConstants.Collector.EMAIL_SEND_ERROR);
584                String formattedMessage = MessageFormat.format(errorMessage, new Object[] { batch.getEmailAddress() });
585                collectorReportData.setEmailSendingStatusForParsedBatch(batch, formattedMessage);
586            }
587        }
588        
589        /**
590         * Sends email message to batch mailing list notifying of email send failures during the collector processing
591         * 
592         * @param collectorReportData - data from collector run
593         */
594        protected void sendEmailSendFailureNotice(CollectorReportData collectorReportData) {
595            MailMessage message = new MailMessage();
596    
597            message.setFromAddress(mailService.getBatchMailingList());
598    
599            String subject = configurationService.getPropertyString(KFSKeyConstants.ERROR_COLLECTOR_EMAILSEND_NOTIFICATION_SUBJECT);
600            String productionEnvironmentCode = configurationService.getPropertyString(KFSConstants.PROD_ENVIRONMENT_CODE_KEY);
601            String environmentCode = configurationService.getPropertyString(KFSConstants.ENVIRONMENT_KEY);
602            if (!StringUtils.equals(productionEnvironmentCode, environmentCode)) {
603                subject = environmentCode + ": " + subject;
604            }
605            message.setSubject(subject);
606    
607            boolean hasEmailSendErrors = false;
608    
609            String body = configurationService.getPropertyString(KFSKeyConstants.ERROR_COLLECTOR_EMAILSEND_NOTIFICATION_BODY);
610            for (String batchId : collectorReportData.getEmailSendingStatus().keySet()) {
611                String emailStatus = collectorReportData.getEmailSendingStatus().get(batchId);
612                if (StringUtils.containsIgnoreCase(emailStatus, "error")) {
613                    body += "Batch: " + batchId + " - " + emailStatus + "\n";
614                    hasEmailSendErrors = true;
615                }
616            }
617            message.setMessage(body);
618    
619            message.addToAddress(mailService.getBatchMailingList());
620    
621            try {
622                if (hasEmailSendErrors) {
623                    mailService.sendMessage(message);
624                }
625            }
626            catch (InvalidAddressException e) {
627                LOG.error("sendErrorEmail() Invalid email address. Message not sent", e);
628            }
629        }
630    
631        /**
632         * Creates a section about validation messages
633         * 
634         * @param errorMessages a List of errors that happened during the Collector run
635         * @param batch the data from the Collector file
636         * @param collectorReportData data gathered from the run of the Collector
637         * @return the Validation message body
638         */
639        protected String createValidationMessageBody(List<String> errorMessages, CollectorBatch batch, CollectorReportData collectorReportData) {
640            StringBuilder body = new StringBuilder();
641    
642            MessageMap fileErrorMap = batch.getMessageMap();
643    
644            body.append("Header Information:\n\n");
645            if (!fileErrorMap.containsMessageKey(KFSKeyConstants.ERROR_BATCH_UPLOAD_PARSING_XML)) {
646                appendHeaderInformation(body, batch);
647                appendTotalsInformation(body, batch);
648                appendValidationStatus(body, errorMessages, true, 0);
649            }
650    
651            return body.toString();
652        }
653    
654        /**
655         * Generates a String that reports on the validation status of the document
656         * 
657         * @param errorMessages a List of error messages encountered in the Collector process
658         * @param notifyIfSuccessful true if a special message for the process running successfully should be added, false otherwise
659         * @param numLeftPaddingSpaces the number of spaces to pad on the left
660         * @return a String with the validation status message
661         */
662        protected String getValidationStatus(List<String> errorMessages, boolean notifyIfSuccessful, int numLeftPaddingSpaces) {
663            StringBuilder buf = new StringBuilder();
664            appendValidationStatus(buf, errorMessages, notifyIfSuccessful, numLeftPaddingSpaces);
665            return buf.toString();
666        }
667    
668        /**
669         * Appends the validation status message to a buffer
670         * 
671         * @param buf a StringBuilder to append error messages to
672         * @param errorMessages a List of error messages encountered in the Collector process
673         * @param notifyIfSuccessful true if a special message for the process running successfully should be added, false otherwise
674         * @param numLeftPaddingSpaces the number of spaces to pad on the left
675         */
676        protected void appendValidationStatus(StringBuilder buf, List<String> errorMessages, boolean notifyIfSuccessful, int numLeftPaddingSpaces) {
677            String padding = StringUtils.leftPad("", numLeftPaddingSpaces, ' ');
678    
679            if (notifyIfSuccessful || !errorMessages.isEmpty()) {
680                buf.append("\n").append(padding).append("Reported Errors:\n");
681            }
682    
683            // ERRORS GO HERE
684            if (errorMessages.isEmpty() && notifyIfSuccessful) {
685                buf.append(padding).append("----- NO ERRORS TO REPORT -----\nThis file will be processed by the accounting cycle.\n");
686            }
687            else if (!errorMessages.isEmpty()) {
688                for (String currentMessage : errorMessages) {
689                    buf.append(padding).append(currentMessage + "\n");
690                }
691                buf.append("\n").append(padding).append("----- THIS FILE WAS NOT PROCESSED AND WILL NEED TO BE CORRECTED AND RESUBMITTED -----\n");
692            }
693        }
694    
695        /**
696         * Writes the part of the report about the demerger
697         * 
698         * @param batch the data from the Collector file
699         * @param collectorReportData data gathered from the run of the Collector
700         * @return
701         */
702        protected String createDemergerMessageBody(CollectorBatch batch, CollectorReportData collectorReportData) {
703            StringBuilder buf = new StringBuilder();
704            appendHeaderInformation(buf, batch);
705    
706            Map<Transaction, List<Message>> batchOriginEntryScrubberErrors = collectorReportData.getBatchOriginEntryScrubberErrors(batch);
707    
708            // the keys of the map returned by getTotalsOnInputOriginEntriesAssociatedWithErrorGroup represent all of the error document
709            // groups in the system
710            Map<DocumentGroupData, OriginEntryTotals> errorGroupDocumentTotals = collectorReportData.getTotalsOnInputOriginEntriesAssociatedWithErrorGroup(batch);
711            Set<DocumentGroupData> errorDocumentGroups = null;
712            if (errorGroupDocumentTotals == null) {
713                return null;
714            }
715            errorDocumentGroups = errorGroupDocumentTotals.keySet();
716            if (errorDocumentGroups.isEmpty()) {
717                return null;
718            }
719            else {
720                for (DocumentGroupData errorDocumentGroup : errorDocumentGroups) {
721                    buf.append("Document ").append(errorDocumentGroup.getDocumentNumber()).append(" Rejected Due to Editing Errors.\n");
722                    for (Transaction transaction : batchOriginEntryScrubberErrors.keySet()) {
723                        if (errorDocumentGroup.matchesTransaction(transaction)) {
724                            if (transaction instanceof OriginEntryFull) {
725                                OriginEntryFull entry = (OriginEntryFull) transaction;
726                                buf.append("     Origin Entry: ").append(entry.getLine()).append("\n");
727                                for (Message message : batchOriginEntryScrubberErrors.get(transaction)) {
728                                    buf.append("          ").append(message.getMessage()).append("\n");
729                                }
730                            }
731                        }
732                    }
733                }
734            }
735    
736            return buf.toString();
737        }
738    
739        /**
740         * Gets the mailService attribute.
741         * 
742         * @return Returns the mailService.
743         */
744        public MailService getMailService() {
745            return mailService;
746        }
747    
748        /**
749         * Sets the mailService attribute value.
750         * 
751         * @param mailService The mailService to set.
752         */
753        public void setMailService(MailService mailService) {
754            this.mailService = mailService;
755        }
756    
757        public void setConfigurationService(KualiConfigurationService configurationService) {
758            this.configurationService = configurationService;
759        }
760    
761        public void setParameterService(ParameterService parameterService) {
762            this.parameterService = parameterService;
763        }
764    
765        /**
766         * Sets the collectorReportWriterService attribute value.
767         * @param collectorReportWriterService The collectorReportWriterService to set.
768         */
769        public void setCollectorReportWriterService(ReportWriterService collectorReportWriterService) {
770            this.collectorReportWriterService = collectorReportWriterService;
771        }
772    
773        public void setPreScrubberService(PreScrubberService preScrubberService) {
774            this.preScrubberService = preScrubberService;
775        }
776        
777        protected class KualiDecimalFormatter implements Formattable {
778            private KualiDecimal number;
779            
780            public KualiDecimalFormatter(KualiDecimal numberToFormat) {
781                this.number = numberToFormat;
782            }
783            
784            public void formatTo(Formatter formatter, int flags, int width, int precision) {
785                Map<String, String> settings = new HashMap<String, String>();
786                settings.put(CurrencyFormatter.SHOW_SYMBOL, Boolean.TRUE.toString());
787                org.kuali.rice.kns.web.format.Formatter cf = org.kuali.rice.kns.web.format.Formatter.getFormatter(KualiDecimal.class, settings);
788                formatter.format((String) cf.format(number));
789            }
790        }
791    }