/*
 * 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.pdp.service.impl;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.businessobject.ProjectCode;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.businessobject.SubObjectCode;
import org.kuali.kfs.coa.service.AccountService;
import org.kuali.kfs.coa.service.ObjectCodeService;
import org.kuali.kfs.coa.service.SubAccountService;
import org.kuali.kfs.coa.service.SubObjectCodeService;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.krad.bo.KualiCodeBase;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DataDictionaryService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.MessageMap;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.pdp.PdpConstants;
import org.kuali.kfs.pdp.PdpKeyConstants;
import org.kuali.kfs.pdp.PdpParameterConstants;
import org.kuali.kfs.pdp.PdpPropertyConstants;
import org.kuali.kfs.pdp.businessobject.AccountingChangeCode;
import org.kuali.kfs.pdp.businessobject.CustomerProfile;
import org.kuali.kfs.pdp.businessobject.PayeeType;
import org.kuali.kfs.pdp.businessobject.PaymentAccountDetail;
import org.kuali.kfs.pdp.businessobject.PaymentAccountHistory;
import org.kuali.kfs.pdp.businessobject.PaymentDetail;
import org.kuali.kfs.pdp.businessobject.PaymentFileLoad;
import org.kuali.kfs.pdp.businessobject.PaymentGroup;
import org.kuali.kfs.pdp.businessobject.PaymentStatus;
import org.kuali.kfs.pdp.dataaccess.PaymentFileLoadDao;
import org.kuali.kfs.pdp.service.CustomerProfileService;
import org.kuali.kfs.pdp.service.PaymentFileValidationService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.businessobject.Bank;
import org.kuali.kfs.sys.businessobject.OriginationCode;
import org.kuali.kfs.sys.service.BankService;
import org.kuali.kfs.sys.service.OriginationCodeService;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.RiceKeyConstants;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.doctype.DocumentTypeService;
import org.springframework.transaction.annotation.Transactional;

import java.sql.Date;
import java.sql.Timestamp;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * @see org.kuali.kfs.pdp.batch.service.PaymentFileValidationService
 */
@Transactional
public class PaymentFileValidationServiceImpl implements PaymentFileValidationService {
    public static final String[] PAYMENT_GROUP_PROPERTIES_TO_CHECK_MAX_LENGTH = {"line1Address", "line2Address", "line3Address", "line4Address", "city", "state", "country", "zipCd", "adviceEmailAddress"};

    protected CustomerProfileService customerProfileService;
    protected PaymentFileLoadDao paymentFileLoadDao;
    protected ParameterService parameterService;
    protected ConfigurationService kualiConfigurationService;
    protected DateTimeService dateTimeService;
    protected AccountService accountService;
    protected SubAccountService subAccountService;
    protected ObjectCodeService objectCodeService;
    protected SubObjectCodeService subObjectCodeService;
    protected BankService bankService;
    protected OriginationCodeService originationCodeService;
    protected DocumentTypeService documentTypeService;
    protected BusinessObjectService businessObjectService;
    protected DataDictionaryService dataDictionaryService;

    /**
     * @see org.kuali.kfs.pdp.batch.service.PaymentFileValidationService#doHardEdits(org.kuali.kfs.pdp.businessobject.PaymentFile,
     * org.kuali.rice.krad.util.MessageMap)
     */
    @Override
    public void doHardEdits(PaymentFileLoad paymentFile, MessageMap errorMap) {
        processHeaderValidation(paymentFile, errorMap);

        if (errorMap.hasNoErrors()) {
            processGroupValidation(paymentFile, errorMap);
        }

        if (errorMap.hasNoErrors()) {
            processTrailerValidation(paymentFile, errorMap);
        }
    }

    /**
     * Validates payment file header fields <li>Checks customer exists in customer profile table and is active</li>
     *
     * @param paymentFile payment file object
     * @param errorMap    map in which errors will be added to
     */
    protected void processHeaderValidation(PaymentFileLoad paymentFile, MessageMap errorMap) {
        CustomerProfile customer = customerProfileService.get(paymentFile.getChart(), paymentFile.getUnit(), paymentFile.getSubUnit());
        if (customer == null) {
            errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_INVALID_CUSTOMER, paymentFile.getChart(), paymentFile.getUnit(), paymentFile.getSubUnit());
        } else {
            if (!customer.isActive()) {
                errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_INACTIVE_CUSTOMER, paymentFile.getChart(), paymentFile.getUnit(), paymentFile.getSubUnit());
            } else {
                paymentFile.setCustomer(customer);
            }
        }
    }

    /**
     * Validates payment file trailer fields <li>Reconciles actual to expected payment count and totals</li> <li>Verifies the batch
     * is not a duplicate</li>
     *
     * @param paymentFile payment file object
     * @param errorMap    map in which errors will be added to
     */
    protected void processTrailerValidation(PaymentFileLoad paymentFile, MessageMap errorMap) {
        // compare trailer payment count to actual count loaded
        if (paymentFile.getActualPaymentCount() != paymentFile.getPaymentCount()) {
            errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_PAYMENT_COUNT_MISMATCH, Integer.toString(paymentFile.getPaymentCount()), Integer.toString(paymentFile.getActualPaymentCount()));
        }

        // compare trailer total amount with actual total amount
        if (paymentFile.getCalculatedPaymentTotalAmount().compareTo(paymentFile.getPaymentTotalAmount()) != 0) {
            errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_PAYMENT_TOTAL_MISMATCH, paymentFile.getPaymentTotalAmount().toString(), paymentFile.getCalculatedPaymentTotalAmount().toString());
        }

        // Check to see if this is a duplicate batch
        Timestamp now = new Timestamp(paymentFile.getCreationDate().getTime());

        if (paymentFileLoadDao.isDuplicateBatch(paymentFile.getCustomer(), paymentFile.getPaymentCount(), paymentFile.getPaymentTotalAmount().bigDecimalValue(), now)) {
            errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_DUPLICATE_BATCH);
        }
    }

    /**
     * Validates payment file groups <li>Checks number of note lines needed is not above the configured maximum allowed</li> <li>
     * Verifies group total is not negative</li> <li>Verifies detail accounting total equals net payment amount</li>
     *
     * @param paymentFile payment file object
     * @param errorMap    map in which errors will be added to
     */
    protected void processGroupValidation(PaymentFileLoad paymentFile, MessageMap errorMap) {
        int groupCount = 0;
        for (PaymentGroup paymentGroup : paymentFile.getPaymentGroups()) {
            groupCount++;
            int noteLineCount = 0;
            int detailCount = 0;

            // We've encountered Payment Files that have address lines exceeding the column size in DB table;
            // so adding extra validation on payment group BO, especially the max length, based on DD definitions.
            // Check that PaymentGroup String properties don't exceed maximum allowed length
            checkPaymentGroupPropertyMaxLength(paymentGroup, errorMap);

            // verify payee id and owner code if customer requires them to be filled in
            if (paymentFile.getCustomer().getPayeeIdRequired() && StringUtils.isBlank(paymentGroup.getPayeeId())) {
                errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_PAYEE_ID_REQUIRED, Integer.toString(groupCount));
            }

            if (paymentFile.getCustomer().getOwnershipCodeRequired() && StringUtils.isBlank(paymentGroup.getPayeeOwnerCd())) {
                errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_PAYEE_OWNER_CODE, Integer.toString(groupCount));
            }

            // validate payee id type
            if (StringUtils.isNotBlank(paymentGroup.getPayeeIdTypeCd())) {
                PayeeType payeeType = businessObjectService.findBySinglePrimaryKey(PayeeType.class, paymentGroup.getPayeeIdTypeCd());
                if (payeeType == null) {
                    errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_INVALID_PAYEE_ID_TYPE, Integer.toString(groupCount), paymentGroup.getPayeeIdTypeCd());
                }
            }

            // validate bank
            String bankCode = paymentGroup.getBankCode();
            if (StringUtils.isNotBlank(bankCode)) {
                Bank bank = bankService.getByPrimaryId(bankCode);
                if (bank == null) {
                    errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_INVALID_BANK_CODE, Integer.toString(groupCount), bankCode);
                } else if (!bank.isActive()) {
                    errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_INACTIVE_BANK_CODE, Integer.toString(groupCount), bankCode);
                }
            }

            KualiDecimal groupTotal = KualiDecimal.ZERO;
            for (PaymentDetail paymentDetail : paymentGroup.getPaymentDetails()) {
                detailCount++;

                noteLineCount++; // Add a line to print the invoice number
                noteLineCount = noteLineCount + paymentDetail.getNotes().size();

                if ((paymentDetail.getNetPaymentAmount() == null) && (!paymentDetail.isDetailAmountProvided())) {
                    paymentDetail.setNetPaymentAmount(paymentDetail.getAccountTotal());
                } else if ((paymentDetail.getNetPaymentAmount() == null) && (paymentDetail.isDetailAmountProvided())) {
                    paymentDetail.setNetPaymentAmount(paymentDetail.getCalculatedPaymentAmount());
                }

                // compare net to accounting segments
                if (paymentDetail.getAccountTotal().compareTo(paymentDetail.getNetPaymentAmount()) != 0) {
                    errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_DETAIL_TOTAL_MISMATCH, Integer.toString(groupCount), Integer.toString(detailCount), paymentDetail.getAccountTotal().toString(), paymentDetail.getNetPaymentAmount().toString());
                }

                // validate origin code if given
                if (StringUtils.isNotBlank(paymentDetail.getFinancialSystemOriginCode())) {
                    OriginationCode originationCode = originationCodeService.getByPrimaryKey(paymentDetail.getFinancialSystemOriginCode());
                    if (originationCode == null) {
                        errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_INVALID_ORIGIN_CODE, Integer.toString(groupCount), Integer.toString(detailCount), paymentDetail.getFinancialSystemOriginCode());
                    }
                }

                // validate doc type if given
                if (StringUtils.isNotBlank(paymentDetail.getFinancialDocumentTypeCode())) {
                    if (!documentTypeService.isActiveByName(paymentDetail.getFinancialDocumentTypeCode())) {
                        errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_INVALID_DOC_TYPE, Integer.toString(groupCount), Integer.toString(detailCount), paymentDetail.getFinancialDocumentTypeCode());
                    }
                }

                groupTotal = groupTotal.add(paymentDetail.getNetPaymentAmount());
            }

            // verify total for group is not negative
            if (groupTotal.doubleValue() < 0) {
                errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_NEGATIVE_GROUP_TOTAL, Integer.toString(groupCount));
            }

            // check that the number of detail items and note lines will fit on a check stub
            if (noteLineCount > getMaxNoteLines()) {
                errorMap.putError(KFSConstants.GLOBAL_ERRORS, PdpKeyConstants.ERROR_PAYMENT_LOAD_MAX_NOTE_LINES, Integer.toString(groupCount), Integer.toString(noteLineCount), Integer.toString(getMaxNoteLines()));
            }
        }
    }

    /**
     * Checks the max length for PaymentGroup properties that could possibly exceed the maximum length defined in DD.
     * This checking is needed because we've encountered Payment Files that have address lines exceeding the column size in DB table, and this causes
     * SQL exceptions while saving PaymentGroup. So we need check all PaymentGroup property values which might exceed the maximum length allowed.
     *
     * @param paymentGroup the payment group for which field lengths are checked
     * @param errorMap     map in which errors will be added to
     */
    protected void checkPaymentGroupPropertyMaxLength(PaymentGroup paymentGroup, MessageMap errorMap) {
        for (String propertyName : PAYMENT_GROUP_PROPERTIES_TO_CHECK_MAX_LENGTH) {
            // we only check max length on String type properties
            String propertyValue = (String) ObjectUtils.getPropertyValue(paymentGroup, propertyName);
            if (StringUtils.isNotEmpty(propertyValue)) {
                // we assume that max length defined in DD is the same as the size of the column in PaymentGroup table
                Integer maxLength = dataDictionaryService.getAttributeMaxLength(PaymentGroup.class, propertyName);
                if ((maxLength != null) && (maxLength.intValue() < propertyValue.length())) {
                    String errorLabel = dataDictionaryService.getAttributeErrorLabel(PaymentGroup.class, propertyName);
                    errorLabel += " with the value '" + propertyValue + "'";
                    errorMap.putError(KFSConstants.GLOBAL_ERRORS, RiceKeyConstants.ERROR_MAX_LENGTH, new String[]{errorLabel, maxLength.toString()});
                }
            }
        }
    }

    /**
     * @see org.kuali.kfs.pdp.service.PaymentFileValidationService#doSoftEdits(org.kuali.kfs.pdp.businessobject.PaymentFile)
     */
    @Override
    public List<String> doSoftEdits(PaymentFileLoad paymentFile) {
        List<String> warnings = new ArrayList<String>();

        CustomerProfile customer = customerProfileService.get(paymentFile.getChart(), paymentFile.getUnit(), paymentFile.getSubUnit());

        // check payment amount does not exceed the configured threshold amount of this customer
        if (paymentFile.getPaymentTotalAmount().compareTo(customer.getFileThresholdAmount()) > 0) {
            addWarningMessage(warnings, PdpKeyConstants.MESSAGE_PAYMENT_LOAD_FILE_THRESHOLD, paymentFile.getPaymentTotalAmount().toString(), customer.getFileThresholdAmount().toString());
            paymentFile.setFileThreshold(true);
        }

        processGroupSoftEdits(paymentFile, customer, warnings);

        return warnings;
    }

    /**
     * Set defaults for group fields and do tax checks.
     *
     * @param paymentFile payment file object
     * @param customer    payment customer
     * @param warnings    <code>List</code> list of accumulated warning messages
     */
    public void processGroupSoftEdits(PaymentFileLoad paymentFile, CustomerProfile customer, List<String> warnings) {
        PaymentStatus openStatus = businessObjectService.findBySinglePrimaryKey(PaymentStatus.class, PdpConstants.PaymentStatusCodes.OPEN);

        for (PaymentGroup paymentGroup : paymentFile.getPaymentGroups()) {
            paymentGroup.setBatchId(paymentFile.getBatchId());
            paymentGroup.setPaymentStatusCode(openStatus.getCode());
            paymentGroup.setPaymentStatus(openStatus);
            paymentGroup.setPayeeName(paymentGroup.getPayeeName().toUpperCase());

            // Set defaults for missing information
            defaultGroupIndicators(paymentGroup);

            // Tax Group Requirements for automatic Holding
            checkForTaxEmailRequired(paymentFile, paymentGroup, customer);

            // KFSMI-9997 / KFSMI-9998
            // Checks for valid payment date or set to tomorrow if missing
            checkGroupPaymentDate(paymentGroup, warnings);

            // do edits on detail lines
            for (PaymentDetail paymentDetail : paymentGroup.getPaymentDetails()) {
                paymentDetail.setPaymentGroupId(paymentGroup.getId());

                processDetailSoftEdits(paymentFile, customer, paymentDetail, warnings);
            }

        }
    }

    /**
     * Set default fields on detail line and check amount against customer threshold.
     *
     * @param paymentFile   payment file object
     * @param customer      payment customer
     * @param paymentDetail <code>PaymentDetail</code> object to process
     * @param warnings      <code>List</code> list of accumulated warning messages
     */
    protected void processDetailSoftEdits(PaymentFileLoad paymentFile, CustomerProfile customer, PaymentDetail paymentDetail, List<String> warnings) {
        updateDetailAmounts(paymentDetail);

        // Check net payment amount
        KualiDecimal testAmount = paymentDetail.getNetPaymentAmount();
        if (testAmount.compareTo(customer.getPaymentThresholdAmount()) > 0) {
            addWarningMessage(warnings, PdpKeyConstants.MESSAGE_PAYMENT_LOAD_DETAIL_THRESHOLD, testAmount.toString(), customer.getPaymentThresholdAmount().toString());
            paymentFile.setDetailThreshold(true);
            paymentFile.getThresholdPaymentDetails().add(paymentDetail);
        }

        // set invoice date if it doesn't exist
        if (paymentDetail.getInvoiceDate() == null) {
            paymentDetail.setInvoiceDate(dateTimeService.getCurrentSqlDate());
        }

        if (paymentDetail.getPrimaryCancelledPayment() == null) {
            paymentDetail.setPrimaryCancelledPayment(Boolean.FALSE);
        }

        // do accounting edits
        for (PaymentAccountDetail paymentAccountDetail : paymentDetail.getAccountDetail()) {
            paymentAccountDetail.setPaymentDetailId(paymentDetail.getId());

            processAccountSoftEdits(paymentFile, customer, paymentAccountDetail, warnings);
        }
    }

    /**
     * Set default fields on account line and perform account field existence checks
     *
     * @param paymentFile          payment file object
     * @param customer             payment customer
     * @param paymentAccountDetail <code>PaymentAccountDetail</code> object to process
     * @param warnings             <code>List</code> list of accumulated warning messages
     */
    protected void processAccountSoftEdits(PaymentFileLoad paymentFile, CustomerProfile customer, PaymentAccountDetail paymentAccountDetail, List<String> warnings) {
        List<PaymentAccountHistory> changeRecords = paymentAccountDetail.getAccountHistory();

        // uppercase chart
        paymentAccountDetail.setFinChartCode(paymentAccountDetail.getFinChartCode().toUpperCase());

        // only do accounting edits if required by customer
        if (customer.getAccountingEditRequired()) {
            // check account number
            Account account = accountService.getByPrimaryId(paymentAccountDetail.getFinChartCode(), paymentAccountDetail.getAccountNbr());
            if (account == null) {
                addWarningMessage(warnings, PdpKeyConstants.MESSAGE_PAYMENT_LOAD_INVALID_ACCOUNT, paymentAccountDetail.getFinChartCode(), paymentAccountDetail.getAccountNbr());

                KualiCodeBase objChangeCd = businessObjectService.findBySinglePrimaryKey(AccountingChangeCode.class, PdpConstants.AccountChangeCodes.INVALID_ACCOUNT);
                replaceAccountingString(objChangeCd, changeRecords, customer, paymentAccountDetail);
            } else {
                // check sub account code
                if (StringUtils.isNotBlank(paymentAccountDetail.getSubAccountNbr())) {
                    SubAccount subAccount = subAccountService.getByPrimaryId(paymentAccountDetail.getFinChartCode(), paymentAccountDetail.getAccountNbr(), paymentAccountDetail.getSubAccountNbr());
                    if (subAccount == null) {
                        addWarningMessage(warnings, PdpKeyConstants.MESSAGE_PAYMENT_LOAD_INVALID_SUB_ACCOUNT, paymentAccountDetail.getFinChartCode(), paymentAccountDetail.getAccountNbr(), paymentAccountDetail.getSubAccountNbr());

                        KualiCodeBase objChangeCd = businessObjectService.findBySinglePrimaryKey(AccountingChangeCode.class, PdpConstants.AccountChangeCodes.INVALID_SUB_ACCOUNT);
                        changeRecords.add(newAccountHistory(PdpPropertyConstants.SUB_ACCOUNT_DB_COLUMN_NAME, KFSConstants.getDashSubAccountNumber(), paymentAccountDetail.getSubAccountNbr(), objChangeCd));

                        paymentAccountDetail.setSubAccountNbr(KFSConstants.getDashSubAccountNumber());
                    }
                }

                // check object code
                ObjectCode objectCode = objectCodeService.getByPrimaryIdForCurrentYear(paymentAccountDetail.getFinChartCode(), paymentAccountDetail.getFinObjectCode());
                if (objectCode == null) {
                    addWarningMessage(warnings, PdpKeyConstants.MESSAGE_PAYMENT_LOAD_INVALID_OBJECT, paymentAccountDetail.getFinChartCode(), paymentAccountDetail.getFinObjectCode());

                    KualiCodeBase objChangeCd = businessObjectService.findBySinglePrimaryKey(AccountingChangeCode.class, PdpConstants.AccountChangeCodes.INVALID_OBJECT);
                    replaceAccountingString(objChangeCd, changeRecords, customer, paymentAccountDetail);
                }

                // check sub object code
                else if (StringUtils.isNotBlank(paymentAccountDetail.getFinSubObjectCode())) {
                    SubObjectCode subObjectCode = subObjectCodeService.getByPrimaryIdForCurrentYear(paymentAccountDetail.getFinChartCode(), paymentAccountDetail.getAccountNbr(), paymentAccountDetail.getFinObjectCode(), paymentAccountDetail.getFinSubObjectCode());
                    if (subObjectCode == null) {
                        addWarningMessage(warnings, PdpKeyConstants.MESSAGE_PAYMENT_LOAD_INVALID_SUB_OBJECT, paymentAccountDetail.getFinChartCode(), paymentAccountDetail.getAccountNbr(), paymentAccountDetail.getFinObjectCode(), paymentAccountDetail.getFinSubObjectCode());

                        KualiCodeBase objChangeCd = businessObjectService.findBySinglePrimaryKey(AccountingChangeCode.class, PdpConstants.AccountChangeCodes.INVALID_SUB_OBJECT);
                        changeRecords.add(newAccountHistory(PdpPropertyConstants.SUB_OBJECT_DB_COLUMN_NAME, KFSConstants.getDashFinancialSubObjectCode(), paymentAccountDetail.getFinSubObjectCode(), objChangeCd));

                        paymentAccountDetail.setFinSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
                    }
                }
            }

            // check project code
            if (StringUtils.isNotBlank(paymentAccountDetail.getProjectCode())) {
                ProjectCode projectCode = businessObjectService.findBySinglePrimaryKey(ProjectCode.class, paymentAccountDetail.getProjectCode());
                if (projectCode == null) {
                    addWarningMessage(warnings, PdpKeyConstants.MESSAGE_PAYMENT_LOAD_INVALID_PROJECT, paymentAccountDetail.getProjectCode());

                    KualiCodeBase objChangeCd = businessObjectService.findBySinglePrimaryKey(AccountingChangeCode.class, PdpConstants.AccountChangeCodes.INVALID_PROJECT);
                    changeRecords.add(newAccountHistory(PdpPropertyConstants.PROJECT_DB_COLUMN_NAME, KFSConstants.getDashProjectCode(), paymentAccountDetail.getProjectCode(), objChangeCd));
                    paymentAccountDetail.setProjectCode(KFSConstants.getDashProjectCode());
                }
            }
        }

        // change nulls into ---'s for the fields that need it
        if (StringUtils.isBlank(paymentAccountDetail.getFinSubObjectCode())) {
            paymentAccountDetail.setFinSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        }

        if (StringUtils.isBlank(paymentAccountDetail.getSubAccountNbr())) {
            paymentAccountDetail.setSubAccountNbr(KFSConstants.getDashSubAccountNumber());
        }

        if (StringUtils.isBlank(paymentAccountDetail.getProjectCode())) {
            paymentAccountDetail.setProjectCode(KFSConstants.getDashProjectCode());
        }

    }

    /**
     * Replaces the entire accounting string with defaults from the customer profile.
     *
     * @param objChangeCd          code indicating reason for change
     * @param changeRecords        <code>List</code> of <code>PaymentAccountHistory</code> records
     * @param customer             profile of payment customer
     * @param paymentAccountDetail account detail record
     */
    protected void replaceAccountingString(KualiCodeBase objChangeCd, List<PaymentAccountHistory> changeRecords, CustomerProfile customer, PaymentAccountDetail paymentAccountDetail) {
        changeRecords.add(newAccountHistory(PdpPropertyConstants.CHART_DB_COLUMN_NAME, customer.getDefaultChartCode(), paymentAccountDetail.getFinChartCode(), objChangeCd));
        changeRecords.add(newAccountHistory(PdpPropertyConstants.ACCOUNT_DB_COLUMN_NAME, customer.getDefaultAccountNumber(), paymentAccountDetail.getAccountNbr(), objChangeCd));
        changeRecords.add(newAccountHistory(PdpPropertyConstants.SUB_ACCOUNT_DB_COLUMN_NAME, customer.getDefaultSubAccountNumber(), paymentAccountDetail.getSubAccountNbr(), objChangeCd));
        changeRecords.add(newAccountHistory(PdpPropertyConstants.OBJECT_DB_COLUMN_NAME, customer.getDefaultObjectCode(), paymentAccountDetail.getFinObjectCode(), objChangeCd));
        changeRecords.add(newAccountHistory(PdpPropertyConstants.SUB_OBJECT_DB_COLUMN_NAME, customer.getDefaultSubObjectCode(), paymentAccountDetail.getFinSubObjectCode(), objChangeCd));

        paymentAccountDetail.setFinChartCode(customer.getDefaultChartCode());
        paymentAccountDetail.setAccountNbr(customer.getDefaultAccountNumber());
        if (StringUtils.isNotBlank(customer.getDefaultSubAccountNumber())) {
            paymentAccountDetail.setSubAccountNbr(customer.getDefaultSubAccountNumber());
        } else {
            paymentAccountDetail.setSubAccountNbr(KFSConstants.getDashSubAccountNumber());
        }
        paymentAccountDetail.setFinObjectCode(customer.getDefaultObjectCode());
        if (StringUtils.isNotBlank(customer.getDefaultSubAccountNumber())) {
            paymentAccountDetail.setFinSubObjectCode(customer.getDefaultSubObjectCode());
        } else {
            paymentAccountDetail.setFinSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        }
    }

    /**
     * Helper method to construct a new payment account history record
     *
     * @param attName    name of field that has changed
     * @param newValue   new value for the field
     * @param oldValue   field value that was changed
     * @param changeCode code indicating reason for change
     * @return <code>PaymentAccountHistory</code>
     */
    protected PaymentAccountHistory newAccountHistory(String attName, String newValue, String oldValue, KualiCodeBase changeCode) {
        PaymentAccountHistory paymentAccountHistory = new PaymentAccountHistory();

        paymentAccountHistory.setAcctAttributeName(attName);
        paymentAccountHistory.setAcctAttributeNewValue(newValue);
        paymentAccountHistory.setAcctAttributeOrigValue(oldValue);
        paymentAccountHistory.setAcctChangeDate(dateTimeService.getCurrentTimestamp());
        paymentAccountHistory.setAccountingChange((AccountingChangeCode) changeCode);

        return paymentAccountHistory;
    }

    /**
     * Sets null amount fields to 0
     *
     * @param paymentDetail <code>PaymentDetail</code> to update
     */
    protected void updateDetailAmounts(PaymentDetail paymentDetail) {
        KualiDecimal zero = KualiDecimal.ZERO;

        if (paymentDetail.getInvTotDiscountAmount() == null) {
            paymentDetail.setInvTotDiscountAmount(zero);
        }

        if (paymentDetail.getInvTotShipAmount() == null) {
            paymentDetail.setInvTotShipAmount(zero);
        }

        if (paymentDetail.getInvTotOtherDebitAmount() == null) {
            paymentDetail.setInvTotOtherDebitAmount(zero);
        }

        if (paymentDetail.getInvTotOtherCreditAmount() == null) {
            paymentDetail.setInvTotOtherCreditAmount(zero);
        }

        // update the total payment amount with the amount from the accounts if null
        if (paymentDetail.getNetPaymentAmount() == null) {
            paymentDetail.setNetPaymentAmount(paymentDetail.getAccountTotal());
        }

        if (paymentDetail.getOrigInvoiceAmount() == null) {
            KualiDecimal amt = paymentDetail.getNetPaymentAmount();
            amt = amt.add(paymentDetail.getInvTotDiscountAmount());
            amt = amt.subtract(paymentDetail.getInvTotShipAmount());
            amt = amt.subtract(paymentDetail.getInvTotOtherDebitAmount());
            amt = amt.add(paymentDetail.getInvTotOtherCreditAmount());
            paymentDetail.setOrigInvoiceAmount(amt);
        }
    }

    /**
     * Sets null indicators to false
     *
     * @param paymentGroup <code>PaymentGroup</code> to update
     */
    protected void defaultGroupIndicators(PaymentGroup paymentGroup) {
        // If combineGroups isn't specified, we want it to default to true
        if (paymentGroup.getCombineGroups() == null) {
            paymentGroup.setCombineGroups(Boolean.TRUE);
        }

        if (paymentGroup.getCampusAddress() == null) {
            paymentGroup.setCampusAddress(Boolean.FALSE);
        }

        if (paymentGroup.getPymtAttachment() == null) {
            paymentGroup.setPymtAttachment(Boolean.FALSE);
        }

        if (paymentGroup.getPymtSpecialHandling() == null) {
            paymentGroup.setPymtSpecialHandling(Boolean.FALSE);
        }

        if (paymentGroup.getProcessImmediate() == null) {
            paymentGroup.setProcessImmediate(Boolean.FALSE);
        }

        if (paymentGroup.getEmployeeIndicator() == null) {
            paymentGroup.setEmployeeIndicator(Boolean.FALSE);
        }

        if (paymentGroup.getNraPayment() == null) {
            paymentGroup.setNraPayment(Boolean.FALSE);
        }

        if (paymentGroup.getTaxablePayment() == null) {
            paymentGroup.setTaxablePayment(Boolean.FALSE);
        }
    }

    /**
     * Checks whether payment status should be set to held and a tax email sent indicating so
     *
     * @param paymentFile  payment file object
     * @param paymentGroup <code>PaymentGroup</code> being checked
     * @param customer     payment customer
     */
    protected void checkForTaxEmailRequired(PaymentFileLoad paymentFile, PaymentGroup paymentGroup, CustomerProfile customer) {
        PaymentStatus heldForNRAEmployee = businessObjectService.findBySinglePrimaryKey(PaymentStatus.class, PdpConstants.PaymentStatusCodes.HELD_TAX_NRA_EMPL_CD);
        PaymentStatus heldForEmployee = businessObjectService.findBySinglePrimaryKey(PaymentStatus.class, PdpConstants.PaymentStatusCodes.HELD_TAX_EMPLOYEE_CD);
        PaymentStatus heldForNRA = businessObjectService.findBySinglePrimaryKey(PaymentStatus.class, PdpConstants.PaymentStatusCodes.HELD_TAX_NRA_CD);

        if (customer.getNraReview() && customer.getEmployeeCheck() && paymentGroup.getEmployeeIndicator().booleanValue() && paymentGroup.getNraPayment().booleanValue()) {
            paymentGroup.setPaymentStatus(heldForNRAEmployee);
            paymentFile.setTaxEmailRequired(true);
        } else if (customer.getEmployeeCheck() && paymentGroup.getEmployeeIndicator().booleanValue()) {
            paymentGroup.setPaymentStatus(heldForEmployee);
            paymentFile.setTaxEmailRequired(true);
        } else if (customer.getNraReview() && paymentGroup.getNraPayment().booleanValue()) {
            paymentGroup.setPaymentStatus(heldForNRA);
            paymentFile.setTaxEmailRequired(true);
        }
    }

    /**
     * Checks the payment date is not more than 30 days past or 30 days coming
     *
     * @param paymentGroup <code>PaymentGroup</code> being checked
     * @param warnings     <code>List</code> list of accumulated warning messages
     */
    protected void checkGroupPaymentDate(PaymentGroup paymentGroup, List<String> warnings) {
        Timestamp now = dateTimeService.getCurrentTimestamp();

        Calendar nowPlus30 = Calendar.getInstance();
        nowPlus30.setTime(now);
        nowPlus30.add(Calendar.DATE, 30);

        Calendar nowMinus30 = Calendar.getInstance();
        nowMinus30.setTime(now);
        nowMinus30.add(Calendar.DATE, -30);

        if (paymentGroup.getPaymentDate() != null) {
            Calendar payDate = Calendar.getInstance();
            payDate.setTime(paymentGroup.getPaymentDate());

            if (payDate.before(nowMinus30)) {
                addWarningMessage(warnings, PdpKeyConstants.MESSAGE_PAYMENT_LOAD_PAYDATE_OVER_30_DAYS_PAST, dateTimeService.toDateString(paymentGroup.getPaymentDate()));
            }

            if (payDate.after(nowPlus30)) {
                addWarningMessage(warnings, PdpKeyConstants.MESSAGE_PAYMENT_LOAD_PAYDATE_OVER_30_DAYS_OUT, dateTimeService.toDateString(paymentGroup.getPaymentDate()));
            }
        } else {
            // KFSMI-9997
            // Calculate tomorrow's date to set as payment date rather than null
            Calendar tomorrow = Calendar.getInstance();
            tomorrow.setTime(now);
            tomorrow.add(Calendar.DATE, 1);
            tomorrow.getTime();

            Date paymentDate = new Date(tomorrow.getTime().getTime());
            paymentGroup.setPaymentDate(paymentDate);
        }
    }

    /**
     * @return system parameter value giving the maximum number of notes allowed.
     */
    protected int getMaxNoteLines() {
        String maxLines = parameterService.getParameterValueAsString(KfsParameterConstants.PRE_DISBURSEMENT_ALL.class, PdpParameterConstants.MAX_NOTE_LINES);
        if (StringUtils.isBlank(maxLines)) {
            throw new RuntimeException("System parameter for max note lines is blank");
        }

        return Integer.parseInt(maxLines);
    }

    /**
     * Helper method for substituting message parameters and adding the message to the warning list.
     *
     * @param warnings   <code>List</code> of messages to add to
     * @param messageKey resource key for message
     * @param arguments  message substitute parameters
     */
    protected void addWarningMessage(List<String> warnings, String messageKey, String... arguments) {
        // Add to global warnings so they will show up on the Payment File Batch Upload screen if
        // the payment file was loaded via that screen
        GlobalVariables.getMessageMap().putWarning(KFSConstants.GLOBAL_MESSAGES, messageKey, arguments);

        String message = kualiConfigurationService.getPropertyValueAsString(messageKey);
        warnings.add(MessageFormat.format(message, (Object[]) arguments));
    }

    /**
     * Sets the customerProfileService attribute value.
     *
     * @param customerProfileService The customerProfileService to set.
     */
    public void setCustomerProfileService(CustomerProfileService customerProfileService) {
        this.customerProfileService = customerProfileService;
    }

    /**
     * Sets the paymentFileLoadDao attribute value.
     *
     * @param paymentFileLoadDao The paymentFileLoadDao to set.
     */
    public void setPaymentFileLoadDao(PaymentFileLoadDao paymentFileLoadDao) {
        this.paymentFileLoadDao = paymentFileLoadDao;
    }

    /**
     * Sets the parameterService attribute value.
     *
     * @param parameterService The parameterService to set.
     */
    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    /**
     * Sets the dateTimeService attribute value.
     *
     * @param dateTimeService The dateTimeService to set.
     */
    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    /**
     * Sets the accountService attribute value.
     *
     * @param accountService The accountService to set.
     */
    public void setAccountService(AccountService accountService) {
        this.accountService = accountService;
    }

    /**
     * Sets the subAccountService attribute value.
     *
     * @param subAccountService The subAccountService to set.
     */
    public void setSubAccountService(SubAccountService subAccountService) {
        this.subAccountService = subAccountService;
    }

    /**
     * Sets the objectCodeService attribute value.
     *
     * @param objectCodeService The objectCodeService to set.
     */
    public void setObjectCodeService(ObjectCodeService objectCodeService) {
        this.objectCodeService = objectCodeService;
    }

    /**
     * Sets the subObjectCodeService attribute value.
     *
     * @param subObjectCodeService The subObjectCodeService to set.
     */
    public void setSubObjectCodeService(SubObjectCodeService subObjectCodeService) {
        this.subObjectCodeService = subObjectCodeService;
    }

    /**
     * Sets the kualiConfigurationService attribute value.
     *
     * @param kualiConfigurationService The kualiConfigurationService to set.
     */
    public void setConfigurationService(ConfigurationService kualiConfigurationService) {
        this.kualiConfigurationService = kualiConfigurationService;
    }

    /**
     * Sets the bankService attribute value.
     *
     * @param bankService The bankService to set.
     */
    public void setBankService(BankService bankService) {
        this.bankService = bankService;
    }

    /**
     * Sets the originationCodeService attribute value.
     *
     * @param originationCodeService The originationCodeService to set.
     */
    public void setOriginationCodeService(OriginationCodeService originationCodeService) {
        this.originationCodeService = originationCodeService;
    }

    /**
     * Gets the businessObjectService attribute.
     *
     * @return Returns the businessObjectService.
     */
    protected BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    /**
     * Sets the businessObjectService attribute value.
     *
     * @param businessObjectService The businessObjectService to set.
     */
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

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

    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
        this.dataDictionaryService = dataDictionaryService;
    }

}
