/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.module.ar.document;
import static org.kuali.kfs.sys.fixture.UserNameFixture.khuntley;
import java.sql.Date;
import java.util.ArrayList;
import java.util.List;
import org.kuali.kfs.module.ar.ArConstants;
import org.kuali.kfs.module.ar.businessobject.AccountsReceivableDocumentHeader;
import org.kuali.kfs.module.ar.businessobject.CashControlDetail;
import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.InvoicePaidApplied;
import org.kuali.kfs.module.ar.businessobject.NonAppliedHolding;
import org.kuali.kfs.module.ar.businessobject.NonInvoiced;
import org.kuali.kfs.module.ar.document.service.CashControlDocumentService;
import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentTestUtil;
import org.kuali.kfs.module.ar.document.service.PaymentApplicationDocumentService;
import org.kuali.kfs.module.ar.fixture.CustomerInvoiceDetailFixture;
import org.kuali.kfs.module.ar.fixture.CustomerInvoiceDocumentFixture;
import org.kuali.kfs.sys.ConfigureContext;
import org.kuali.kfs.sys.DocumentTestUtils;
import org.kuali.kfs.sys.businessobject.ChartOrgHolder;
import org.kuali.kfs.sys.context.KualiTestBase;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.fixture.UserNameFixture;
import org.kuali.kfs.sys.service.FinancialSystemUserService;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.krad.UserSession;
import org.kuali.rice.krad.exception.ValidationException;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.service.DocumentService;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.MessageMap;
@ConfigureContext(session = khuntley)
public class PaymentApplicationDocumentTest extends KualiTestBase {
static protected final String ANNOTATION = "PaymentApplicationDocument testing";
static protected final String TESTING = "PaymentApplicationDocument testing";
protected DocumentService documentService;
protected BusinessObjectService businessObjectService;
protected CashControlDocumentService cashControlDocumentService;
protected PaymentApplicationDocumentService paymentApplicationDocumentService;
protected DateTimeService dateTimeService;
protected UniversityDateService universityDateService;
@Override
protected void setUp() throws Exception {
super.setUp();
documentService = SpringContext.getBean(DocumentService.class);
businessObjectService = SpringContext.getBean(BusinessObjectService.class);
cashControlDocumentService = SpringContext.getBean(CashControlDocumentService.class);
paymentApplicationDocumentService = SpringContext.getBean(PaymentApplicationDocumentService.class);
dateTimeService = SpringContext.getBean(DateTimeService.class);
universityDateService = SpringContext.getBean(UniversityDateService.class);
}
// Test that payment application documents are created for each cash control detail
public void testCreatedUponApprovingCashControlDocument() throws Exception {
List<CashControlDetailSpec> specs = new ArrayList<CashControlDetailSpec>();
specs.add(new CashControlDetailSpec("ABB2","9999",new Date(System.currentTimeMillis()),new KualiDecimal(1000)));
CashControlDocument cashControlDocument = createAndSaveNewCashControlDocument(specs);
for(CashControlDetail detail : cashControlDocument.getCashControlDetails()) {
assertNotNull("Reference financial document should not have been null on line: " + detail, detail.getReferenceFinancialDocument());
}
}
public void testExactlyApplyingSucceeds() throws Exception {
// Set our customer up to owe $1
CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
new CustomerInvoiceDetailFixture[] { CustomerInvoiceDetailFixture.ONE_DOLLAR_INVOICE_DETAIL };
// Receive a payment of $1 from the Customer.
CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[] {
CashControlDetailSpec.specFor(new KualiDecimal(1))
};
KualiDecimal[] amountsToApply = new KualiDecimal[] {
new KualiDecimal(1)
};
assertTrue(applyFundsToPaymentApplication(invoiceDetailFixtures,cashControlDetailSpecs,amountsToApply,null,null));
}
// FIXME TODO This should succeed when saving but fail when submitting.
public void testUnderApplyingFailsWithoutUnapplied() throws Exception {
// Set our customer up to owe $10
CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
new CustomerInvoiceDetailFixture[] { CustomerInvoiceDetailFixture.TEN_DOLLAR_INVOICE_DETAIL };
// Receive a payment of $10 from the Customer.
CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[] {
CashControlDetailSpec.specFor(new KualiDecimal(10))
};
KualiDecimal[] amountsToApply = new KualiDecimal[] {
new KualiDecimal(1)
};
assertTrue(applyFundsToPaymentApplication(invoiceDetailFixtures,cashControlDetailSpecs,amountsToApply,null,null));
assertFalse(applyFundsToPaymentApplication(invoiceDetailFixtures,cashControlDetailSpecs,amountsToApply,null,null,true,false));
}
public void testUnderApplyingSucceedsWithUnapplied() throws Exception {
// Set our customer up to owe $10
CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
new CustomerInvoiceDetailFixture[] { CustomerInvoiceDetailFixture.TEN_DOLLAR_INVOICE_DETAIL };
// Receive a payment of $10 from the Customer.
CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[] {
CashControlDetailSpec.specFor(new KualiDecimal(10))
};
KualiDecimal[] amountsToApply = new KualiDecimal[] {
new KualiDecimal(1)
};
KualiDecimal[] unappliedAmounts = new KualiDecimal[] {
new KualiDecimal(9)
};
assertTrue(applyFundsToPaymentApplication(invoiceDetailFixtures,cashControlDetailSpecs,amountsToApply,null,null));
}
public void testOverApplyingFails() throws Exception {
// Set our customer up to owe $1
CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
new CustomerInvoiceDetailFixture[] { CustomerInvoiceDetailFixture.ONE_DOLLAR_INVOICE_DETAIL };
// Receive a payment of $2 from the Customer.
CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[] {
CashControlDetailSpec.specFor(new KualiDecimal(2))
};
KualiDecimal[] amountsToApply = new KualiDecimal[] {
new KualiDecimal(2)
};
assertFalse(applyFundsToPaymentApplication(invoiceDetailFixtures,cashControlDetailSpecs,amountsToApply,null,null));
}
public void testAddingNonInvoicedLinesSucceedsWhenBalanced() throws Exception {
// Set our customer up to owe $10
CustomerInvoiceDetailFixture[] invoiceDetailFixtures =
new CustomerInvoiceDetailFixture[] { CustomerInvoiceDetailFixture.TEN_DOLLAR_INVOICE_DETAIL };
// Receive a payment of $2 from the Customer.
CashControlDetailSpec[] cashControlDetailSpecs = new CashControlDetailSpec[] {
CashControlDetailSpec.specFor(new KualiDecimal(2))
};
NonInvoiced[] nonInvoiceds = new NonInvoiced[] {
buildNonInvoiced("BL","0212002","0795",new KualiDecimal(2))
};
assertTrue(applyFundsToPaymentApplication(invoiceDetailFixtures,cashControlDetailSpecs,null,null,nonInvoiceds));
}
// ------
// ------ Utility methods. Non test methods.
// ------
@Override
protected void changeCurrentUser(UserNameFixture sessionUser) throws Exception {
GlobalVariables.setUserSession(new UserSession(sessionUser.toString()));
}
protected CashControlDocument createAndSaveNewCashControlDocument(CashControlDetailSpec[] specs) throws WorkflowException {
List<CashControlDetailSpec> _specs = new ArrayList<CashControlDetailSpec>();
for(CashControlDetailSpec spec : specs) {
_specs.add(spec);
}
return createAndSaveNewCashControlDocument(_specs);
}
protected CashControlDocument createAndSaveNewCashControlDocument(List<CashControlDetailSpec> specs) throws WorkflowException {
CashControlDocument cashControlDocument = createNewCashControlDocument(specs);
documentService.saveDocument(cashControlDocument);
return cashControlDocument;
}
protected CashControlDocument createNewCashControlDocument(CashControlDetailSpec[] specs) throws WorkflowException {
List<CashControlDetailSpec> _specs = new ArrayList<CashControlDetailSpec>();
for(CashControlDetailSpec spec : specs) {
_specs.add(spec);
}
return createNewCashControlDocument(_specs);
}
protected CashControlDocument createNewCashControlDocument(List<CashControlDetailSpec> specs) throws WorkflowException {
CashControlDocument cashControlDocument = DocumentTestUtils.createDocument(SpringContext.getBean(DocumentService.class), CashControlDocument.class);// documentService.getNewDocument(CashControlDocument.class);
cashControlDocument.getDocumentHeader().setDocumentDescription(TESTING);
cashControlDocument.setUniversityFiscalYear(universityDateService.getCurrentFiscalYear());
cashControlDocument.setCustomerPaymentMediumCode("CK");
AccountsReceivableDocumentHeader arDocumentHeader = cashControlDocument.getAccountsReceivableDocumentHeader();
arDocumentHeader.setDocumentNumber(cashControlDocument.getDocumentNumber());
// Set the processing chart and org
UserSession userSession = GlobalVariables.getUserSession();
ChartOrgHolder organization = SpringContext.getBean(FinancialSystemUserService.class).getPrimaryOrganization(userSession.getPerson(), ArConstants.AR_NAMESPACE_CODE);
arDocumentHeader.setProcessingChartOfAccountCode(organization.getChartOfAccountsCode());
arDocumentHeader.setProcessingOrganizationCode(organization.getOrganizationCode());
MessageMap e = GlobalVariables.getMessageMap();
int errorCount = e.getNumberOfPropertiesWithErrors();
try {
documentService.saveDocument(cashControlDocument);
} catch(Exception t) {
fail( "Document save failed: " + t.getClass().getName() + " : " + t.getMessage() + "\n" + dumpMessageMapErrors() + "\n" + cashControlDocument );
}
for(CashControlDetailSpec spec : specs) {
CashControlDetail cashControlDetail = buildCashControlDetail(cashControlDocument,spec);
cashControlDocumentService.addNewCashControlDetail(TESTING, cashControlDocument, cashControlDetail);
PaymentApplicationDocument paymentApplicationDocument = cashControlDocumentService.createAndSavePaymentApplicationDocument(TESTING, cashControlDocument, cashControlDetail);
cashControlDetail.setReferenceFinancialDocumentNumber(paymentApplicationDocument.getDocumentNumber());
}
return cashControlDocument;
}
protected CashControlDetail buildCashControlDetail(CashControlDocument cashControlDocument, CashControlDetailSpec spec) {
CashControlDetail cashControlDetail = new CashControlDetail();
cashControlDetail.setCashControlDocument(cashControlDocument);
cashControlDetail.setDocumentNumber(cashControlDocument.getDocumentNumber());
cashControlDetail.setCustomerNumber(spec.customerNumber);
cashControlDetail.setCustomerPaymentMediumIdentifier(spec.customerPaymentMediumIdentifier);
cashControlDetail.setCustomerPaymentDate(spec.customerPaymentDate);
cashControlDetail.setFinancialDocumentLineAmount(spec.financialDocumentLineAmount);
return cashControlDetail;
}
public InvoiceAndCashControlDocumentPair createCashControlDocument(CustomerInvoiceDetailFixture[] invoiceDetailFixtures, CashControlDetailSpec[] cashControlDetailSpecs) throws WorkflowException {
// Create an invoice
CustomerInvoiceDocument invoice =
CustomerInvoiceDocumentTestUtil.submitNewCustomerInvoiceDocumentAndReturnIt(
CustomerInvoiceDocumentFixture.BASE_CIDOC_WITH_CUSTOMER, invoiceDetailFixtures, null);
// Create a cash control document as well.
CashControlDocument cashControlDocument = createAndSaveNewCashControlDocument(cashControlDetailSpecs);
return new InvoiceAndCashControlDocumentPair(invoice,cashControlDocument);
}
/**
* This method will apply funds and save the payment application documents.
* It will not route or approve the documents.
*
* @param invoiceDetailFixtures
* @param cashControlDetailSpecs
* @param amountsToApply
* @param unappliedAmounts
* @param nonInvoiceds
* @return
* @throws Exception
*/
public boolean applyFundsToPaymentApplication(CustomerInvoiceDetailFixture[] invoiceDetailFixtures, CashControlDetailSpec[] cashControlDetailSpecs, KualiDecimal[] amountsToApply, KualiDecimal[] unappliedAmounts, NonInvoiced[] nonInvoiceds) throws Exception {
return applyFundsToPaymentApplication(invoiceDetailFixtures,cashControlDetailSpecs,amountsToApply,unappliedAmounts,nonInvoiceds,false,false);
}
/**
* This method will create save, route and approve payment application documents as specified.
*
* @param invoiceDetailFixtures
* @param cashControlDetailSpecs
* @param amountsToApply
* @param unappliedAmounts
* @param nonInvoiceds
* @param routeDocument
* @param approveDocument
* @return
* @throws Exception
*/
public boolean applyFundsToPaymentApplication(CustomerInvoiceDetailFixture[] invoiceDetailFixtures, CashControlDetailSpec[] cashControlDetailSpecs, KualiDecimal[] amountsToApply, KualiDecimal[] unappliedAmounts, NonInvoiced[] nonInvoiceds, boolean routeDocument, boolean approveDocument) throws Exception {
// Verify that we have an amount to apply to each invoice detail.
if(null != amountsToApply && cashControlDetailSpecs.length != amountsToApply.length) {
throw new Exception("The number of cash control detail specs must equal the number of amounts to apply.");
}
if(null != unappliedAmounts && cashControlDetailSpecs.length != unappliedAmounts.length) {
throw new Exception("The number of cash control detail specs must equal the number of unapplied amounts.");
}
if(null != nonInvoiceds && cashControlDetailSpecs.length != nonInvoiceds.length) {
throw new Exception("The number of cash control detail specs must equal the number of non-invoiced lines.");
}
// Create the invoice and cash control document we need to be able to test the payment application document.
InvoiceAndCashControlDocumentPair pair = createCashControlDocument(invoiceDetailFixtures,cashControlDetailSpecs);
CashControlDocument cashControlDocument = pair.cashControlDocument;
CustomerInvoiceDocument invoice = pair.invoiceDocument;
assertNotNull( "invoiceDocument of the InvoiceAndCashControlDocumentPair must not be null", invoice );
// Get convenient handles to the various relevant details.
List<CashControlDetail> cashControlDetails = cashControlDocument.getCashControlDetails();
List<CustomerInvoiceDetail> customerInvoiceDetails = invoice.getSourceAccountingLines();
// Pick a sample invoice detail that we can apply payments to.
CustomerInvoiceDetail sampleInvoiceDetail = customerInvoiceDetails.iterator().next();
// Now try to apply too much money in receivables (cash control details) against the the outstanding balance (sample invoice detail)
// counter allows us to match amounts to apply with specific invoice details
int counter = 0;
// aggregateOperationSucceeds allows us to measure the success of all operations in aggregate
boolean aggregateOperationSucceeds = true;
// Each cash control detail has a reference to a payment application document which records
// the receipt of funds. To test the payment application document we modify these payment application
// documents according to the arguments passed into this method.
for(CashControlDetail cashControlDetail : cashControlDetails) {
// payments are credit against the customer balance via the payment application document referenced from the cash control document.
PaymentApplicationDocument paymentApplicationDocument = cashControlDetail.getReferenceFinancialDocument();
// ------ Set invoice paid applieds
// Make sure we've got an amount to apply directly to this document
if(null != amountsToApply && counter < amountsToApply.length && null != amountsToApply[counter]) {
// Create a new applied payment
// Applying one big payment is just as good as applying a bunch of smaller payments from a testing perspective
InvoicePaidApplied invoicePaidApplied = new InvoicePaidApplied();
// set the document number for the invoice paid applied to the payment application document number.
invoicePaidApplied.setDocumentNumber(paymentApplicationDocument.getDocumentNumber());
// Set the invoice paid applied ref doc number to the document number for the customer invoice document
invoicePaidApplied.setFinancialDocumentReferenceInvoiceNumber(invoice.getDocumentNumber());
// Apply this payment to the sample invoice detail
invoicePaidApplied.setInvoiceItemNumber(sampleInvoiceDetail.getInvoiceItemNumber());
// Apply too much money (double the amount owed)
// sampleInvoiceDetail.getAmount().multiply(new KualiDecimal(2))
invoicePaidApplied.setInvoiceItemAppliedAmount(amountsToApply[counter]);
invoicePaidApplied.setUniversityFiscalYear(universityDateService.getCurrentFiscalYear());
invoicePaidApplied.setUniversityFiscalPeriodCode(universityDateService.getCurrentUniversityDate().getUniversityFiscalAccountingPeriod());
invoicePaidApplied.setPaidAppliedItemNumber(cashControlDetailSpecs.length);
// if there was not another invoice paid applied already created for the current detail then invoicePaidApplied will not be null
if (invoicePaidApplied != null) {
// add it to the payment application document list of applied payments
paymentApplicationDocument.getInvoicePaidApplieds().add(invoicePaidApplied);
// set the new applied amount for the customer invoice detail
//sampleInvoiceDetail.setAmountToBeApplied(invoicePaidApplied.getInvoiceItemAppliedAmount());
}
}
// ------ Set the unapplied amount
// Make sure we've got an unapplied amount specified for this detail
if(null != unappliedAmounts && counter < unappliedAmounts.length && null != unappliedAmounts[counter]) {
NonAppliedHolding nonAppliedHolding = new NonAppliedHolding();
nonAppliedHolding.setFinancialDocumentLineAmount(unappliedAmounts[counter]);
nonAppliedHolding.setReferenceFinancialDocumentNumber(paymentApplicationDocument.getDocumentNumber());
paymentApplicationDocument.setNonAppliedHolding(nonAppliedHolding);
}
// ------ Set the non-invoiced amount
int nonInvoicedLineCounter = 1;
if(null != nonInvoiceds && counter < nonInvoiceds.length && null != nonInvoiceds[counter]) {
NonInvoiced nonInvoiced = nonInvoiceds[counter];
nonInvoiced.setFinancialDocumentPostingYear(paymentApplicationDocument.getPostingYear());
nonInvoiced.setDocumentNumber(paymentApplicationDocument.getDocumentNumber());
nonInvoiced.setFinancialDocumentLineNumber(nonInvoicedLineCounter++);
paymentApplicationDocument.getNonInvoiceds().add(nonInvoiced);
}
// Try to save the document
try {
documentService.saveDocument(paymentApplicationDocument);
} catch(ValidationException validationException) {
// Indicate a failures
aggregateOperationSucceeds &= false;
}
// Try to route the document
if(routeDocument) {
try {
documentService.routeDocument(paymentApplicationDocument, "Unit tests", new ArrayList());
} catch(ValidationException validationException) {
// Indicate a failures
aggregateOperationSucceeds &= false;
}
}
// Try to approve the document
if(approveDocument) {
try {
documentService.approveDocument(paymentApplicationDocument, "Unit tests", new ArrayList());
} catch(ValidationException validationException) {
// Indicate a failures
aggregateOperationSucceeds &= false;
}
}
}
return aggregateOperationSucceeds;
}
protected NonInvoiced buildNonInvoiced(String chartOfAccountsCode, String accountNumber, String financialObjectCode, KualiDecimal financialDocumentLineAmount) {
NonInvoiced nonInvoiced = new NonInvoiced();
nonInvoiced.setChartOfAccountsCode(chartOfAccountsCode);
nonInvoiced.setAccountNumber(accountNumber);
nonInvoiced.setFinancialObjectCode(financialObjectCode);
nonInvoiced.setFinancialDocumentLineAmount(financialDocumentLineAmount);
return nonInvoiced;
}
protected class InvoiceAndCashControlDocumentPair {
CashControlDocument cashControlDocument;
CustomerInvoiceDocument invoiceDocument;
InvoiceAndCashControlDocumentPair() {};
InvoiceAndCashControlDocumentPair(CustomerInvoiceDocument invoice, CashControlDocument cashControl) {
this.invoiceDocument = invoice;
this.cashControlDocument = cashControl;
}
}
protected class NonInvoicedLineSpec {
String chartCode;
String accountNumber;
String objectCode;
KualiDecimal amount;
NonInvoicedLineSpec() {}
NonInvoicedLineSpec(String chartCode, String accountNumber, String objectCode, KualiDecimal amount) {
this.chartCode = chartCode;
this.accountNumber = accountNumber;
this.objectCode = objectCode;
this.amount = amount;
}
}
}