/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 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.module.ar.batch.service.impl;

import com.lowagie.text.Chunk;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.pdf.PdfWriter;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.kns.service.DataDictionaryService;
import org.kuali.kfs.krad.UserSession;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.ar.ArConstants;
import org.kuali.kfs.module.ar.ArPropertyConstants;
import org.kuali.kfs.module.ar.batch.service.LockboxService;
import org.kuali.kfs.module.ar.businessobject.AccountsReceivableDocumentHeader;
import org.kuali.kfs.module.ar.businessobject.CashControlDetail;
import org.kuali.kfs.module.ar.businessobject.Customer;
import org.kuali.kfs.module.ar.businessobject.Lockbox;
import org.kuali.kfs.module.ar.businessobject.SystemInformation;
import org.kuali.kfs.module.ar.dataaccess.LockboxDao;
import org.kuali.kfs.module.ar.document.CashControlDocument;
import org.kuali.kfs.module.ar.document.CustomerInvoiceDocument;
import org.kuali.kfs.module.ar.document.PaymentApplicationDocument;
import org.kuali.kfs.module.ar.document.service.AccountsReceivableDocumentHeaderService;
import org.kuali.kfs.module.ar.document.service.CashControlDocumentService;
import org.kuali.kfs.module.ar.document.service.CustomerService;
import org.kuali.kfs.module.ar.document.service.PaymentApplicationDocumentService;
import org.kuali.kfs.module.ar.document.service.SystemInformationService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.service.NonTransactional;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.KewApiServiceLocator;
import org.kuali.rice.kew.api.doctype.DocumentType;
import org.kuali.rice.kew.api.doctype.DocumentTypeService;
import org.kuali.rice.kew.api.document.attribute.DocumentAttributeIndexingQueue;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;
import org.springframework.transaction.annotation.Transactional;

import java.awt.*;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * Lockbox Iterators are sorted by processedInvoiceDate and batchSequenceNumber.
 * Potentially there could be many batches on the same date.
 * For each set of records with the same processedInvoiceDate and batchSequenceNumber,
 * there will be one Cash-Control document. Each record within this set will create one Application document.
 */


public class LockboxServiceImpl implements LockboxService {
    private static Logger LOG = org.apache.log4j.Logger.getLogger(LockboxServiceImpl.class);
    ;

    private DocumentService documentService;
    private SystemInformationService systemInformationService;
    private AccountsReceivableDocumentHeaderService accountsReceivableDocumentHeaderService;
    private CashControlDocumentService cashControlDocumentService;
    private PaymentApplicationDocumentService payAppDocService;
    private DataDictionaryService dataDictionaryService;
    private DateTimeService dateTimeService;
    private BusinessObjectService boService;
    private CustomerService customerService;
    private DocumentTypeService documentTypeService;
    private LockboxDao lockboxDao;
    private String reportsDirectory;

    Lockbox ctrlLockbox;
    CashControlDocument cashControlDocument;
    boolean anyRecordsFound = false;


    @Override
    @NonTransactional
    public boolean processLockboxes() throws WorkflowException {

        ctrlLockbox = new Lockbox();
        cashControlDocument = null;
        anyRecordsFound = false;
        try {
            //  create the pdf doc
            com.lowagie.text.Document pdfdoc = getPdfDoc();

            //  this giant try/catch is to make sure that something gets written to the
            // report.  please dont use it for specific exception handling, rather nest
            // new try/catch handlers inside this.
            try {
                Iterator<Lockbox> itr = getAllLockboxes().iterator();

                while (itr.hasNext()) {
                    processLockbox(itr.next(), pdfdoc);
                }
                //  if we have a cashControlDocument here, then it needs to be routed, its the last one
                if (cashControlDocument != null) {
                    LOG.info("   routing cash control document.");

                    //documentService.routeDocument(cashControlDocument, "Routed by Lockbox Batch process.", null);
                    cashControlDocument.getDocumentHeader().getWorkflowDocument().route("Routed by Lockbox Batch process.");

                    DocumentType documentType = documentTypeService.getDocumentTypeByName(cashControlDocument.getFinancialDocumentTypeCode());
                    DocumentAttributeIndexingQueue queue = KewApiServiceLocator.getDocumentAttributeIndexingQueue(documentType.getApplicationId());
                    queue.indexDocument(cashControlDocument.getDocumentNumber());
                }

                //  if no records were found, write something useful to the report
                if (!anyRecordsFound) {
                    writeDetailLine(pdfdoc, "NO LOCKBOX RECORDS WERE FOUND");
                }

                //  this annoying all-encompassing try/catch is here to make sure that the report gets
                // written.  without it, if anything goes wrong, the report will end up a zero-byte document.
            } catch (Exception e) {
                writeDetailLine(pdfdoc, "AN EXCEPTION OCCURRED:");
                writeDetailLine(pdfdoc, "");
                writeDetailLine(pdfdoc, e.getMessage());
                writeDetailLine(pdfdoc, "");
                writeExceptionStackTrace(pdfdoc, e);

                throw new RuntimeException("An exception occured while processing Lockboxes.", e);
            } finally {
                //  spool the report
                if (pdfdoc != null) {
                    pdfdoc.close();
                }
            }
        } catch (IOException | DocumentException ex) {
            throw new RuntimeException("Could not open file for lockbox processing results report", ex);
        }
        return true;

    }

    @Transactional
    protected Collection<Lockbox> getAllLockboxes() {
        Collection<Lockbox> itr = lockboxDao.getAllLockboxes();
        return itr;
    }

    @Override
    @Transactional
    public void processLockbox(Lockbox lockbox, com.lowagie.text.Document pdfdoc) {
        anyRecordsFound = true;
        LOG.info("LOCKBOX: '" + lockbox.getLockboxNumber() + "'");

        //  retrieve the processingOrg (system information) for this lockbox number
        SystemInformation sysInfo = systemInformationService.getByLockboxNumberForCurrentFiscalYear(lockbox.getLockboxNumber());
        String initiator = sysInfo.getFinancialDocumentInitiatorIdentifier();
        LOG.info("   using SystemInformation: '" + sysInfo.toString() + "'");
        LOG.info("   using Financial Document Initiator ID: '" + initiator + "'");

        //  puke if the initiator stored in the systemInformation table is no good
        Principal principal = KimApiServiceLocator.getIdentityService().getPrincipal(initiator);
        if (principal == null) {
            LOG.warn("   could not find [" + initiator + "] when searching by PrincipalID, so trying to find as a PrincipalName.");
            principal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(initiator);
            if (principal == null) {
                LOG.error("Financial Document Initiator ID [" + initiator + "] specified in SystemInformation [" + sysInfo.toString() + "] for Lockbox Number " + lockbox.getLockboxNumber() + " is not present in the system as either a PrincipalID or a PrincipalName.");
                throw new RuntimeException("Financial Document Initiator ID [" + initiator + "] specified in SystemInformation [" + sysInfo.toString() + "] for Lockbox Number " + lockbox.getLockboxNumber() + " is not present in the system as either a PrincipalID or a PrincipalName.");
            } else {
                LOG.info("   found [" + initiator + "] in the system as a PrincipalName.");
            }
        } else {
            LOG.info("   found [" + initiator + "] in the system as a PrincipalID.");
        }

        //  masquerade as the person indicated in the systemInformation
        GlobalVariables.clear();
        GlobalVariables.setUserSession(new UserSession(principal.getPrincipalName()));

        if (lockbox.compareTo(ctrlLockbox) != 0) {
            // If we made it in here, then we have hit a different batchSequenceNumber and processedInvoiceDate.
            // When this is the case, we create a new cashcontroldocument and start tacking subsequent lockboxes on
            // to the current cashcontroldocument as cashcontroldetails.
            LOG.info("New Lockbox batch");

            //  we're creating a new cashcontrol, so if we have an old one, we need to route it
            if (cashControlDocument != null) {
                LOG.info("   routing cash control document.");
                try {

//                    documentService.routeDocument(cashControlDocument, "Routed by Lockbox Batch process.", null);
                    cashControlDocument.getDocumentHeader().getWorkflowDocument().route("Routed by Lockbox Batch process.");

                    //RICE20 replaced searchableAttributeProcessingService.indexDocument with DocumentAttributeIndexingQueue.indexDocument
                    DocumentType documentType = documentTypeService.getDocumentTypeByName(cashControlDocument.getFinancialDocumentTypeCode());
                    DocumentAttributeIndexingQueue queue = KewApiServiceLocator.getDocumentAttributeIndexingQueue(documentType.getApplicationId());
                    queue.indexDocument(cashControlDocument.getDocumentNumber());

                } catch (Exception e) {
                    LOG.error("A Exception was thrown while trying to route the CashControl document.", e);
                    throw new RuntimeException("A Exception was thrown while trying to route the CashControl document.", e);
                }
            }

            //  create a new CashControl document
            LOG.info("Creating new CashControl document for invoice: " + lockbox.getFinancialDocumentReferenceInvoiceNumber() + ".");
            try {
                cashControlDocument = (CashControlDocument) documentService.getNewDocument(KFSConstants.FinancialDocumentTypeCodes.CASH_CONTROL);
            } catch (Exception e) {
                LOG.error("A Exception was thrown while trying to initiate a new CashControl document.", e);
                throw new RuntimeException("A Exception was thrown while trying to initiate a new CashControl document.", e);
            }
            LOG.info("   CashControl documentNumber == '" + cashControlDocument.getDocumentNumber() + "'");

            //  write the batch group header to the report
            writeBatchGroupSectionTitle(pdfdoc, lockbox.getBatchSequenceNumber().toString(), lockbox.getProcessedInvoiceDate(),
                cashControlDocument.getDocumentNumber());

            cashControlDocument.setCustomerPaymentMediumCode(lockbox.getCustomerPaymentMediumCode());
            if (ObjectUtils.isNotNull(lockbox.getBankCode())) {
                String bankCode = lockbox.getBankCode();
                cashControlDocument.setBankCode(bankCode);
            }
            cashControlDocument.getDocumentHeader().setDocumentDescription(ArConstants.LOCKBOX_DOCUMENT_DESCRIPTION + lockbox.getLockboxNumber());

            //  setup the AR header for this CashControl doc
            LOG.info("   creating AR header for customer: [" + lockbox.getCustomerNumber() + "] and ProcessingOrg: " + sysInfo.getProcessingChartOfAccountCode() + "-" + sysInfo.getProcessingOrganizationCode() + ".");
            AccountsReceivableDocumentHeader arDocHeader = new AccountsReceivableDocumentHeader();
            arDocHeader.setProcessingChartOfAccountCode(sysInfo.getProcessingChartOfAccountCode());
            arDocHeader.setProcessingOrganizationCode(sysInfo.getProcessingOrganizationCode());
            arDocHeader.setDocumentNumber(cashControlDocument.getDocumentNumber());

            if (ObjectUtils.isNotNull(lockbox.getCustomerNumber())) {
                Customer customer = customerService.getByPrimaryKey(lockbox.getCustomerNumber());
                if (ObjectUtils.isNotNull(customer)) {
                    arDocHeader.setCustomerNumber(lockbox.getCustomerNumber());
                }
            }
            cashControlDocument.setAccountsReceivableDocumentHeader(arDocHeader);
        }
        // set our control lockbox as the current lockbox and create details.
        ctrlLockbox = lockbox;

        //  write the lockbox detail line to the report
        writeLockboxRecordLine(pdfdoc, lockbox.getLockboxNumber(), lockbox.getCustomerNumber(), lockbox.getFinancialDocumentReferenceInvoiceNumber(),
            lockbox.getInvoicePaidOrAppliedAmount(), lockbox.getCustomerPaymentMediumCode(), lockbox.getBankCode());

        //  skip zero-dollar-amount lockboxes
        if (lockbox.getInvoicePaidOrAppliedAmount().isZero()) {
            LOG.warn("   lockbox has a zero dollar amount, so we're skipping it.");
            writeSummaryDetailLine(pdfdoc, "ZERO-DOLLAR LOCKBOX - NO FURTHER PROCESSING");
            deleteProcessedLockboxEntry(lockbox);
            return;
        }
        if (lockbox.getInvoicePaidOrAppliedAmount().isLessThan(KualiDecimal.ZERO)) {
            LOG.warn("   lockbox has a negative dollar amount, so we're skipping it.");
            writeCashControlDetailLine(pdfdoc, lockbox.getInvoicePaidOrAppliedAmount(), "SKIPPED");
            writeSummaryDetailLine(pdfdoc, "NEGATIVE-DOLLAR LOCKBOX - NO FURTHER PROCESSING - LOCKBOX ENTRY NOT DELETED");
            return;
        }

        //  create a new cashcontrol detail
        CashControlDetail detail = new CashControlDetail();
        if (ObjectUtils.isNotNull(lockbox.getCustomerNumber())) {
            Customer customer = customerService.getByPrimaryKey(lockbox.getCustomerNumber());
            if (ObjectUtils.isNotNull(customer)) {
                detail.setCustomerNumber(lockbox.getCustomerNumber());
            }
        }
        detail.setFinancialDocumentLineAmount(lockbox.getInvoicePaidOrAppliedAmount());
        detail.setCustomerPaymentDate(lockbox.getProcessedInvoiceDate());
        detail.setCustomerPaymentDescription("Lockbox Remittance  " + lockbox.getFinancialDocumentReferenceInvoiceNumber());

        //  add it to the document
        LOG.info("   creating detail for $" + lockbox.getInvoicePaidOrAppliedAmount() + " with invoiceDate: " + lockbox.getProcessedInvoiceDate());

        try {
            cashControlDocumentService.addNewCashControlDetail(ArConstants.LOCKBOX_DOCUMENT_DESCRIPTION, cashControlDocument, detail);
        } catch (WorkflowException e) {
            LOG.error("A Exception was thrown while trying to create a new CashControl detail.", e);
            throw new RuntimeException("A Exception was thrown while trying to create a new CashControl detail.", e);
        }

        //  retrieve the docNumber of the generated payapp
        String payAppDocNumber = detail.getReferenceFinancialDocumentNumber();
        LOG.info("   new PayAppDoc was created: " + payAppDocNumber + ".");

        String invoiceNumber = lockbox.getFinancialDocumentReferenceInvoiceNumber();
        LOG.info("   lockbox references invoice number [" + invoiceNumber + "].");

        //  if thats the case, dont even bother looking for an invoice, just save the CashControl
        if (StringUtils.isBlank(invoiceNumber)) {
            LOG.info("   invoice number is blank; cannot load an invoice.");
            detail.setCustomerPaymentDescription(ArConstants.LOCKBOX_REMITTANCE_FOR_INVALID_INVOICE_NUMBER + lockbox.getFinancialDocumentReferenceInvoiceNumber());
            try {
                documentService.saveDocument(cashControlDocument);
            } catch (WorkflowException e) {
                LOG.error("A Exception was thrown while trying to save the CashControl document.", e);
                throw new RuntimeException("A Exception was thrown while trying to save the CashControl document.", e);
            }

            //KFSMI-6719
            //routePayAppWithoutBusinessRules(payAppDocNumber, "CREATED & SAVED by Lockbox batch");

            //write the detail and payapp lines to the report
            writeCashControlDetailLine(pdfdoc, detail.getFinancialDocumentLineAmount(), detail.getCustomerPaymentDescription());
            writePayAppLine(pdfdoc, detail.getReferenceFinancialDocumentNumber(), "CREATED & SAVED");
            writeSummaryDetailLine(pdfdoc, "INVOICE NUMBER IS BLANK");
            //  delete the lockbox now we're done with it
            deleteProcessedLockboxEntry(lockbox);
            return;
        }

        //  check to see if the invoice indicated exists, and if not, then save the CashControl and move on
        if (!documentService.documentExists(invoiceNumber)) {
            LOG.info("   invoice number [" + invoiceNumber + "] does not exist in system, so cannot load the original invoice.");
            detail.setCustomerPaymentDescription(ArConstants.LOCKBOX_REMITTANCE_FOR_INVALID_INVOICE_NUMBER + lockbox.getFinancialDocumentReferenceInvoiceNumber());
            try {
                documentService.saveDocument(cashControlDocument);
            } catch (WorkflowException e) {
                LOG.error("A Exception was thrown while trying to save the CashControl document.", e);
                throw new RuntimeException("A Exception was thrown while trying to save the CashControl document.", e);
            }

            // KFSCNTRB-1241
            //routePayAppWithoutBusinessRules(payAppDocNumber, "CREATED & SAVED by Lockbox batch");

            writeCashControlDetailLine(pdfdoc, detail.getFinancialDocumentLineAmount(), detail.getCustomerPaymentDescription());
            writePayAppLine(pdfdoc, detail.getReferenceFinancialDocumentNumber(), "CREATED & SAVED");
            writeSummaryDetailLine(pdfdoc, "INVOICE DOESNT EXIST");
            //  delete the lockbox now we're done with it
            deleteProcessedLockboxEntry(lockbox);
            return;
        }

        //  load up the specified invoice from the lockbox
        LOG.info("   loading invoice number [" + invoiceNumber + "].");
        CustomerInvoiceDocument customerInvoiceDocument;
        try {
            customerInvoiceDocument = (CustomerInvoiceDocument) documentService.getByDocumentHeaderId(invoiceNumber);
        } catch (WorkflowException e) {
            LOG.error("A Exception was thrown while trying to load invoice #" + invoiceNumber + ".", e);
            throw new RuntimeException("A Exception was thrown while trying to load invoice #" + invoiceNumber + ".", e);
        }

        //  if the invoice is already closed, then just save the CashControl and move on
        writeInvoiceDetailLine(pdfdoc, invoiceNumber, customerInvoiceDocument.isOpenInvoiceIndicator(),
            customerInvoiceDocument.getCustomer().getCustomerNumber(), customerInvoiceDocument.getOpenAmount());
        if (!customerInvoiceDocument.isOpenInvoiceIndicator()) {
            LOG.info("   invoice is already closed, so saving CashControl doc and moving on.");
            detail.setCustomerPaymentDescription(ArConstants.LOCKBOX_REMITTANCE_FOR_CLOSED_INVOICE_NUMBER + lockbox.getFinancialDocumentReferenceInvoiceNumber());
            try {
                documentService.saveDocument(cashControlDocument);
            } catch (WorkflowException e) {
                LOG.error("A Exception was thrown while trying to save the CashControl document.", e);
                throw new RuntimeException("A Exception was thrown while trying to save the CashControl document.", e);
            }

            // KFSCNTRB-1241
            //routePayAppWithoutBusinessRules(payAppDocNumber, "CREATED & SAVED by Lockbox batch");

            writeCashControlDetailLine(pdfdoc, detail.getFinancialDocumentLineAmount(), detail.getCustomerPaymentDescription());
            writePayAppLine(pdfdoc, detail.getReferenceFinancialDocumentNumber(), "CREATED & SAVED");
            writeSummaryDetailLine(pdfdoc, "INVOICE ALREADY CLOSED");
            deleteProcessedLockboxEntry(lockbox);
            return;
        }

        PaymentApplicationDocument payAppDoc;
        try {
            payAppDoc = (PaymentApplicationDocument) documentService.getByDocumentHeaderId(payAppDocNumber);
        } catch (WorkflowException e) {
            LOG.error("A Exception was thrown while trying to load PayApp #" + payAppDocNumber + ".", e);
            throw new RuntimeException("A Exception was thrown while trying to load PayApp #" + payAppDocNumber + ".", e);
        }

        boolean autoApprove = canAutoApprove(customerInvoiceDocument, lockbox, payAppDoc);
        String annotation = "CREATED & SAVED";

        //  if the lockbox amount matches the invoice amount, then create, save and approve a PayApp, and then
        // mark the invoice
        if (autoApprove) {
            LOG.info("   lockbox amount matches invoice total document amount [" + customerInvoiceDocument.getTotalDollarAmount() + "].");
            annotation = "CREATED, SAVED, and BLANKET APPROVED";

            //  load up the PayApp document that was created
            LOG.info("   loading the generated PayApp [" + payAppDocNumber + "], so we can route or approve it.");


            //  create paidapplieds on the PayApp doc for all the Invoice details
            LOG.info("   attempting to create paidApplieds on the PayAppDoc for every detail on the invoice.");
            payAppDoc = payAppDocService.createInvoicePaidAppliedsForEntireInvoiceDocument(customerInvoiceDocument, payAppDoc);
            LOG.info("   PayAppDoc has TotalApplied of " + payAppDoc.getTotalApplied() + " for a Control Balance of " + payAppDoc.getTotalFromControl() + ".");

            //  Save and approve the payapp doc
            LOG.info("   attempting to blanketApprove the PayApp Doc.");
            try {

                documentService.blanketApproveDocument(payAppDoc, "Automatically approved by Lockbox batch job.", null);
            } catch (WorkflowException e) {
                LOG.error("A Exception was thrown while trying to blanketApprove PayAppDoc #" + payAppDoc.getDocumentNumber() + ".", e);
                throw new RuntimeException("A Exception was thrown while trying to blanketApprove PayAppDoc #" + payAppDoc.getDocumentNumber() + ".", e);
            }

            //  write the report details
            writeCashControlDetailLine(pdfdoc, detail.getFinancialDocumentLineAmount(), detail.getCustomerPaymentDescription());
            writePayAppLine(pdfdoc, detail.getReferenceFinancialDocumentNumber(), annotation);
            writeSummaryDetailLine(pdfdoc, "LOCKBOX AMOUNT MATCHES INVOICE OPEN AMOUNT");
        } else {
            LOG.info("   lockbox amount does NOT match invoice total document amount [" + customerInvoiceDocument.getTotalDollarAmount() + "].");

            // KFSCNTRB-1241
            //routePayAppWithoutBusinessRules(payAppDocNumber, "CREATED & SAVED by Lockbox batch");

            //  write the report details
            writeCashControlDetailLine(pdfdoc, detail.getFinancialDocumentLineAmount(), detail.getCustomerPaymentDescription());
            writePayAppLine(pdfdoc, detail.getReferenceFinancialDocumentNumber(), annotation);
            if (lockbox.getInvoicePaidOrAppliedAmount().isLessThan(customerInvoiceDocument.getOpenAmount())) {
                writeSummaryDetailLine(pdfdoc, "LOCKBOX UNDERPAID INVOICE");
            } else {
                writeSummaryDetailLine(pdfdoc, "LOCKBOX OVERPAID INVOICE");
            }
        }

        //  save the cashcontrol, which saves any changes to the details
        detail.setCustomerPaymentDescription(ArConstants.LOCKBOX_REMITTANCE_FOR_INVOICE_NUMBER + lockbox.getFinancialDocumentReferenceInvoiceNumber());
        LOG.info("   saving cash control document.");
        try {
            documentService.saveDocument(cashControlDocument);
        } catch (WorkflowException e) {
            LOG.error("A Exception was thrown while trying to save the CashControl document.", e);
            throw new RuntimeException("A Exception was thrown while trying to save the CashControl document.", e);
        }

        //  delete the lockbox now we're done with it
        deleteProcessedLockboxEntry(lockbox);
    }

    protected boolean canAutoApprove(CustomerInvoiceDocument invoice, Lockbox lockbox, PaymentApplicationDocument payAppDoc) {
        boolean retVal = invoice.getOpenAmount().equals(lockbox.getInvoicePaidOrAppliedAmount());
        retVal &= ObjectUtils.isNotNull(payAppDoc.getCashControlDetail().getCustomerNumber());
        return retVal;
    }

    protected void routePayAppWithoutBusinessRules(String payAppDocNumber, String annotation) {

        //  load up the PayApp document that was created
        LOG.info("   loading the generated PayApp [" + payAppDocNumber + "], so we can route or approve it.");
        PaymentApplicationDocument payAppDoc;
        try {
            payAppDoc = (PaymentApplicationDocument) documentService.getByDocumentHeaderId(payAppDocNumber);
        } catch (WorkflowException e) {
            LOG.error("A Exception was thrown while trying to load PayApp #" + payAppDocNumber + ".", e);
            throw new RuntimeException("A Exception was thrown while trying to load PayApp #" + payAppDocNumber + ".", e);
        }

        //  route without business rules
        LOG.info("   attempting to route without business rules the PayApp Doc.");
        payAppDoc.getDocumentHeader().getWorkflowDocument().route(annotation);

        DocumentType documentType = documentTypeService.getDocumentTypeByName(payAppDoc.getFinancialDocumentTypeCode());
        DocumentAttributeIndexingQueue queue = KewApiServiceLocator.getDocumentAttributeIndexingQueue(documentType.getApplicationId());
        queue.indexDocument(payAppDoc.getDocumentNumber());
    }

    protected void deleteProcessedLockboxEntry(Lockbox lockboxEntry) {
        Map<String, Object> pkMap = new HashMap<String, Object>();
        pkMap.put(ArPropertyConstants.INVOICE_SEQUENCE_NUMBER, lockboxEntry.getInvoiceSequenceNumber());
        boService.deleteMatching(Lockbox.class, pkMap);
    }

    protected com.lowagie.text.Document getPdfDoc() throws IOException, DocumentException {

        String reportDropFolder = reportsDirectory + "/" + ArConstants.Lockbox.LOCKBOX_REPORT_SUBFOLDER + "/";
        String fileName = ArConstants.Lockbox.BATCH_REPORT_BASENAME + "_" +
            new SimpleDateFormat("yyyyMMdd_HHmmssSSS").format(dateTimeService.getCurrentDate()) + ".pdf";

        //  setup the writer
        File reportFile = new File(reportDropFolder + fileName);
        FileOutputStream fileOutStream;
        fileOutStream = new FileOutputStream(reportFile);
        BufferedOutputStream buffOutStream = new BufferedOutputStream(fileOutStream);

        com.lowagie.text.Document pdfdoc = new com.lowagie.text.Document(PageSize.LETTER, 54, 54, 72, 72);
        PdfWriter.getInstance(pdfdoc, buffOutStream);

        pdfdoc.open();

        return pdfdoc;
    }

    protected String rightPad(String valToPad, int sizeToPadTo) {
        return rightPad(valToPad, sizeToPadTo, " ");
    }

    protected String rightPad(String valToPad, int sizeToPadTo, String padChar) {
        if (StringUtils.isBlank(valToPad)) {
            return StringUtils.repeat(padChar, sizeToPadTo);
        }
        if (valToPad.length() >= sizeToPadTo) {
            return valToPad;
        }
        return valToPad + StringUtils.repeat(padChar, sizeToPadTo - valToPad.length());
    }

    protected void writeBatchGroupSectionTitle(com.lowagie.text.Document pdfDoc, String batchSeqNbr, java.sql.Date procInvDt, String cashControlDocNumber) {
        Font font = FontFactory.getFont(FontFactory.COURIER, 10, Font.BOLD);

        String lineText = "CASHCTL " + rightPad(cashControlDocNumber, 12) + " " +
            "BATCH GROUP: " + rightPad(batchSeqNbr, 5) + " " +
            rightPad((procInvDt == null ? "NONE" : procInvDt.toString()), 35);

        Paragraph paragraph = new Paragraph();
        paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT);
        Chunk chunk = new Chunk(lineText, font);
        chunk.setBackground(Color.LIGHT_GRAY, 5, 5, 5, 5);
        paragraph.add(chunk);

        //  blank line
        paragraph.add(new Chunk("", font));

        try {
            pdfDoc.add(paragraph);
        } catch (DocumentException e) {
            LOG.error("iText DocumentException thrown when trying to write content.", e);
            throw new RuntimeException("iText DocumentException thrown when trying to write content.", e);
        }
    }

    protected void writeLockboxRecordLine(com.lowagie.text.Document pdfDoc, String lockboxNumber, String customerNumber, String invoiceNumber,
                                          KualiDecimal invoiceTotalAmount, String paymentMediumCode, String bankCode) {

        writeDetailLine(pdfDoc, StringUtils.repeat("-", 100));

        StringBuilder sb = new StringBuilder();
        sb.append("   ");                                                        // 3:   1 - 3
        sb.append("LOCKBOX: " + rightPad(lockboxNumber, 10) + " ");              // 20:  4 - 23
        sb.append("CUST: " + rightPad(customerNumber, 9) + " ");                 // 15:  24 - 38
        sb.append("INV: " + rightPad(invoiceNumber, 10) + " ");                  // 16:  39 - 55
        sb.append(StringUtils.repeat(" ", 28));                                  // 28:  56 - 83
        sb.append("AMT: " + rightPad(invoiceTotalAmount.toString(), 11) + " ");  // 17:  84 - 100

        writeDetailLine(pdfDoc, sb.toString());
    }

    protected void writeInvoiceDetailLine(com.lowagie.text.Document pdfDoc, String invoiceNumber, boolean open, String customerNumber, KualiDecimal openAmount) {

        StringBuilder sb = new StringBuilder();
        sb.append("   ");                                                        // 3:   1 - 3
        sb.append("INVOICE: " + rightPad(invoiceNumber, 10) + " ");              // 20:  4 - 23
        sb.append("CUST: " + rightPad(customerNumber, 9) + " ");                 // 15:  24 - 38
        if (open) {
            sb.append(rightPad("OPEN", 16) + " ");                               // 16:  39 - 55
        } else {
            sb.append(rightPad("CLOSED", 16) + " ");                             // 16:  39 - 55
        }
        sb.append(StringUtils.repeat(" ", 22));                                  // 28:  56 - 83
        sb.append("OPEN AMT: " + rightPad(openAmount.toString(), 11) + " ");  // 17:  84 - 100

        writeDetailLine(pdfDoc, sb.toString());
    }

    protected void writeCashControlDetailLine(com.lowagie.text.Document pdfDoc, KualiDecimal amount, String description) {

        StringBuilder sb = new StringBuilder();
        sb.append("   ");                                                        // 3:   1 - 3
        sb.append("CASHCTL DTL: " + rightPad(description, 66) + " ");            // 80:  4 - 83
        sb.append("AMT: " + rightPad(amount.toString(), 11) + " ");              // 17:  84 - 100

        writeDetailLine(pdfDoc, sb.toString());
    }

    protected void writeSummaryDetailLine(com.lowagie.text.Document pdfDoc, String summary) {
        writeDetailLine(pdfDoc, "   " + summary);
    }

    protected void writePayAppLine(com.lowagie.text.Document pdfDoc, String payAppDocNbr, String description) {

        StringBuilder sb = new StringBuilder();
        sb.append("   ");                                                    // 3:   1 - 3
        sb.append("PAYAPP DOC NBR: " + rightPad(payAppDocNbr, 12) + " ");    // 29:  4 - 32
        sb.append("ACTION: " + description);                                 // 40:  33 - 72

        writeDetailLine(pdfDoc, sb.toString());
    }

    protected void writeExceptionStackTrace(com.lowagie.text.Document pdfDoc, Exception e) {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        PrintWriter printWriter = new PrintWriter(outStream, true);

        e.printStackTrace(printWriter);
        printWriter.flush();
        writeDetailLine(pdfDoc, outStream.toString());
    }

    protected void writeDetailLine(com.lowagie.text.Document pdfDoc, String detailLineText) {
        if (ObjectUtils.isNotNull(detailLineText)) {
            Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.NORMAL);

            Paragraph paragraph = new Paragraph();
            paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT);
            paragraph.add(new Chunk(detailLineText, font));

            try {
                pdfDoc.add(paragraph);
            } catch (DocumentException e) {
                LOG.error("iText DocumentException thrown when trying to write content.", e);
                throw new RuntimeException("iText DocumentException thrown when trying to write content.", e);
            }
        }
    }

    @Override
    @Transactional
    public Long getMaxLockboxSequenceNumber() {
        return lockboxDao.getMaxLockboxSequenceNumber();
    }

    @NonTransactional
    public LockboxDao getLockboxDao() {
        return lockboxDao;
    }

    @NonTransactional
    public void setLockboxDao(LockboxDao lockboxDao) {
        this.lockboxDao = lockboxDao;
    }

    @NonTransactional
    public SystemInformationService getSystemInformationService() {
        return systemInformationService;
    }

    @NonTransactional
    public void setSystemInformationService(SystemInformationService systemInformationService) {
        this.systemInformationService = systemInformationService;
    }

    @NonTransactional
    public AccountsReceivableDocumentHeaderService getAccountsReceivableDocumentHeaderService() {
        return accountsReceivableDocumentHeaderService;
    }

    @NonTransactional
    public void setAccountsReceivableDocumentHeaderService(AccountsReceivableDocumentHeaderService accountsReceivableDocumentHeaderService) {
        this.accountsReceivableDocumentHeaderService = accountsReceivableDocumentHeaderService;
    }

    @NonTransactional
    public void setPaymentApplicationDocumentService(PaymentApplicationDocumentService paymentApplicationDocumentService) {
        this.payAppDocService = paymentApplicationDocumentService;
    }

    /**
     * Gets the documentService attribute.
     *
     * @return Returns the documentService.
     */
    @NonTransactional
    public DocumentService getDocumentService() {
        return documentService;
    }

    /**
     * Sets the documentService attribute value.
     *
     * @param documentService The documentService to set.
     */
    @NonTransactional
    public void setDocumentService(DocumentService documentService) {
        this.documentService = documentService;
    }

    /**
     * Sets the dataDictionaryService attribute value.
     *
     * @param dataDictionaryService The dataDictionaryService to set.
     */
    @NonTransactional
    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
        this.dataDictionaryService = dataDictionaryService;
    }

    @NonTransactional
    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    @NonTransactional
    public CashControlDocumentService getCashControlDocumentService() {
        return cashControlDocumentService;
    }

    @NonTransactional
    public void setCashControlDocumentService(CashControlDocumentService cashControlDocumentService) {
        this.cashControlDocumentService = cashControlDocumentService;
    }

    @NonTransactional
    public void setReportsDirectory(String reportsDirectory) {
        this.reportsDirectory = reportsDirectory;
    }

    @NonTransactional
    public void setBoService(BusinessObjectService boService) {
        this.boService = boService;
    }

    /**
     * Gets the customerService attribute.
     *
     * @return Returns the customerService
     */
    @NonTransactional
    public CustomerService getCustomerService() {
        return customerService;
    }

    /**
     * Sets the customerService attribute.
     *
     * @param customerService The customerService to set.
     */
    @NonTransactional
    public void setCustomerService(CustomerService customerService) {
        this.customerService = customerService;
    }

    @NonTransactional
    public void setDocumentTypeService(DocumentTypeService documentTypeService) {
        this.documentTypeService = documentTypeService;
    }
}
