/*
jBilling - The Enterprise Open Source Billing System
Copyright (C) 2003-2011 Enterprise jBilling Software Ltd. and Emiliano Conde
This file is part of jbilling.
jbilling 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.
jbilling 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 jbilling. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sapienter.jbilling.server.pluggableTask;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Random;
import java.util.List;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.log4j.Logger;
import com.sapienter.jbilling.server.item.CurrencyBL;
import com.sapienter.jbilling.server.payment.PaymentAuthorizationBL;
import com.sapienter.jbilling.server.payment.PaymentDTOEx;
import com.sapienter.jbilling.server.payment.db.PaymentAuthorizationDTO;
import com.sapienter.jbilling.server.payment.db.PaymentResultDAS;
import com.sapienter.jbilling.server.pluggableTask.admin.ParameterDescription;
import com.sapienter.jbilling.server.pluggableTask.admin.PluggableTaskException;
import com.sapienter.jbilling.server.user.ContactBL;
import com.sapienter.jbilling.server.user.CreditCardBL;
import com.sapienter.jbilling.server.util.Constants;
import java.util.ArrayList;
public class PaymentAuthorizeNetTask extends PluggableTask
implements PaymentTask {
// pluggable task parameters names
public static final ParameterDescription PARAMETER_LOGIN =
new ParameterDescription("login", true, ParameterDescription.Type.STR);
public static final ParameterDescription PARAMETER_TRANSACTION =
new ParameterDescription("transaction", true, ParameterDescription.Type.STR);
public static final ParameterDescription PARAMETER_TEST =
new ParameterDescription("test", false, ParameterDescription.Type.STR);
public static final ParameterDescription PARAMETER_AVS =
new ParameterDescription("submit_avs", false, ParameterDescription.Type.STR);
//initializer for pluggable params
{
descriptions.add(PARAMETER_LOGIN);
descriptions.add(PARAMETER_TRANSACTION);
descriptions.add(PARAMETER_TEST);
descriptions.add(PARAMETER_AVS);
}
//private static final String url = "https://certification.authorize.net/gateway/transact.dll";
private static final String url = "https://secure.authorize.net/gateway/transact.dll";
private static final int timeOut = 10000; // in millisec
private static final Logger LOG = Logger.getLogger(PaymentAuthorizeNetTask.class);
/* (non-Javadoc)
* @see com.sapienter.jbilling.server.pluggableTask.PaymentTask#process(com.sapienter.betty.server.payment.PaymentDTOEx)
*/
public boolean process(PaymentDTOEx paymentInfo)
throws PluggableTaskException{
boolean retValue = false;
boolean isTest = false;
// authorize.net is not available for payouts to partners
if (paymentInfo.getPayoutId() != null) {
return true; // means that the next task should be processed
}
/*
* Since this communicates via https, java needs a keystore with the client's key
* in order to trust the authroize.net server.
* The java properties javax.net.ssl.trustStore javax.net.ssl.trustStorePassword
* have to be properly set (and the keystore present).
* This can be done as a parameter to the JVM (-D), or right here:
System.setProperty("javax.net.ssl.trustStore", "/dir1/keystore/client.keystore");
System.setProperty("javax.net.ssl.trustStorePassword", "myPassword");
*/
try {
int method = -1; // 1 cc , 2 ach
if (paymentInfo.getCreditCard() == null &&
paymentInfo.getAch() == null) {
LOG.error("Can't process without a credit card or ach");
throw new TaskException("Credit card/ACH not present in payment");
}
if (paymentInfo.getCreditCard() != null) {
method = 1;
}
if (paymentInfo.getAch() != null) {
method = 2;
}
if (paymentInfo.getCreditCard() != null &&
paymentInfo.getAch() != null) {
LOG.warn("Both cc and ach are present");
method = 2; // default to ach (cheaper)
}
if (paymentInfo.getIsRefund() == 1 &&
(paymentInfo.getPayment() == null ||
paymentInfo.getPayment().getAuthorization() ==null)) {
LOG.error("Can't process refund without a payment with an" +
" authorization record");
throw new TaskException("Refund without previous " +
"authorization");
}
String expiry = null;
if (method == 1) {
expiry = CreditCardBL.get4digitExpiry(
paymentInfo.getCreditCard());
}
String login = (String) parameters.get(PARAMETER_LOGIN.getName());
String transaction = (String) parameters.get(PARAMETER_TRANSACTION.getName());
if (login == null || login.length() == 0 || transaction == null ||
transaction.length() == 0) {
throw new TaskException("invalid parameters");
}
String testStr = (String) parameters.get(PARAMETER_TEST.getName());
if (testStr != null) {
isTest = true;
}
// find the currency code of this payment
CurrencyBL currencyBL = new CurrencyBL(
paymentInfo.getCurrency().getId());
String currencyCode = currencyBL.getEntity().getCode();
LOG.debug("making call with " + login + " " + transaction +
" " + expiry);
NameValuePair[] data;
if (method == 1) {
if (paymentInfo.getIsRefund() == 0) {
data = getChargeData(login, transaction, isTest,
paymentInfo.getAmount(),
paymentInfo.getCreditCard().getNumber(), expiry,
currencyCode, true, paymentInfo.getId());
} else {
data = getRefundData(login, transaction, isTest,
paymentInfo.getAmount(),
paymentInfo.getCreditCard().getNumber(), expiry,
paymentInfo.getPayment().getAuthorization().
getTransactionId());
}
} else {
if (paymentInfo.getIsRefund() == 0) {
data = getACHChargeData(login, transaction, isTest,
paymentInfo.getAmount(),
paymentInfo.getAch().getAbaRouting(),
paymentInfo.getAch().getBankAccount(),
paymentInfo.getAch().getAccountType(),
paymentInfo.getAch().getBankName(),
paymentInfo.getAch().getAccountName(),
currencyCode);
} else {
data = getACHRefundData(login, transaction, isTest,
paymentInfo.getAmount(),
paymentInfo.getAch().getAbaRouting(),
paymentInfo.getAch().getBankAccount(),
paymentInfo.getAch().getAccountType(),
paymentInfo.getAch().getBankName(),
paymentInfo.getAch().getAccountName(),
paymentInfo.getPayment().getAuthorization().
getTransactionId());
}
}
// see if AVS info has to be included
String doAvs = (String) parameters.get(PARAMETER_AVS.getName());
if (doAvs != null && doAvs.equals("true")) {
data = addAVSFields(paymentInfo.getUserId(), data);
LOG.debug("returning after avs " + data);
}
AuthorizeNetResponseDTO response = makeCall(data);
paymentInfo.setAuthorization(response.getPaymentAuthorizationDTO());
// the result of this request goes in the dto
if (Integer.valueOf(response.getPaymentAuthorizationDTO().
getCode1()).intValue() == 1) {
paymentInfo.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_OK));
LOG.debug("result is ok");
} else {
// there are actually two other codes, 2 is decalined, but
// 3 is just 'error' may be for a 3 it should just return true
// to try another processor. Now we only do that for exceptions
paymentInfo.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_FAIL));
LOG.debug("result is fail");
}
// now create the db row with the results of this authorization call
PaymentAuthorizationBL bl = new PaymentAuthorizationBL();
// set the processor
response.getPaymentAuthorizationDTO().setProcessor("Authorize.net");
bl.create(response.getPaymentAuthorizationDTO(), paymentInfo.getId());
} catch (HttpException e) {
LOG.warn("Http exception when calling Authorize.net", e);
paymentInfo.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_UNAVAILABLE));
retValue = true;
} catch (IOException e) {
LOG.warn("IO exception when calling Authorize.net", e);
paymentInfo.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_UNAVAILABLE));
retValue = true;
} catch (Exception e) {
LOG.error("Exception", e);
throw new PluggableTaskException(e);
}
// let's make this usefull for testing too
if (isTest) {
LOG.debug("Running Authorize.net task in test mode!");
Random rand = new Random();
paymentInfo.setPaymentResult(new PaymentResultDAS().find(new Integer(rand.nextInt(3) + 1)));
retValue = false;
}
LOG.debug("returning " + retValue);
return retValue;
}
/* (non-Javadoc)
* @see com.sapienter.jbilling.server.pluggableTask.PaymentTask#failure(com.sapienter.betty.interfaces.UserEntityLocal, int)
*/
public void failure(Integer userId, Integer retry) {
}
/*
public static void main(String[] args) {
try {
AuthorizeNetResponseDTO response = makeCall(getChargeData("alphat",
"xxx", true,
new Float(10.1), "4007000000027", "0505", "USD"));
System.out.println("got dto:" + response);
System.out.println("now the code is " + Integer.valueOf(response.getPaymentAuthorizationDTO().
getCode1()).intValue());
} catch (Exception e) {
System.err.println("Got exception " + e);
}
}
*/
public NameValuePair[] getChargeData(String login,
String transaction, boolean test, BigDecimal amount, String cc_number,
String cc_expiry, String currencyCode, boolean isCharge,
Integer paymentId) {
NameValuePair[] data = {
new NameValuePair("x_Version", "3.1"),
new NameValuePair("x_Delim_Data", "TRUE"),
new NameValuePair("x_relay_response", "False"),
new NameValuePair("x_Login", login),
new NameValuePair("x_Tran_Key", transaction),
new NameValuePair("x_Amount", amount.toString()),
new NameValuePair("x_Card_Num", cc_number),
new NameValuePair("x_Exp_Date", cc_expiry),
new NameValuePair("x_Type", isCharge ? "AUTH_CAPTURE" : "AUTH_ONLY"),
new NameValuePair("x_Test_Request", test ? "TRUE" : "FALSE"),
new NameValuePair("x_currency_code", currencyCode),
new NameValuePair("x_invoice_num", paymentId.toString()),
new NameValuePair("x_description", "Invoice number is payment ID")
};
return data;
}
// Since now a refund is done only when linked to a previous payment,
// I assume that the currency is not required
private NameValuePair[] getRefundData(String login,
String transaction, boolean test, BigDecimal amount, String cc_number,
String cc_expiry, String transactionId) {
NameValuePair[] data = {
new NameValuePair("x_Version", "3.1"),
new NameValuePair("x_Delim_Data", "TRUE"),
new NameValuePair("x_relay_response", "False"),
new NameValuePair("x_Login", login),
new NameValuePair("x_Tran_Key", transaction),
new NameValuePair("x_Amount", amount.toString()),
new NameValuePair("x_Card_Num", cc_number),
new NameValuePair("x_Exp_Date", cc_expiry),
new NameValuePair("x_Type", "CREDIT"),
new NameValuePair("x_Test_Request", test ? "TRUE" : "FALSE"),
new NameValuePair("x_Trans_ID", transactionId)
};
return data;
}
private NameValuePair[] getACHChargeData(String login,
String transaction, boolean test, BigDecimal amount,String aba,
String account,
Integer type, String bank, String name, String currencyCode) {
NameValuePair[] data = {
new NameValuePair("x_Version", "3.1"),
new NameValuePair("x_Delim_Data", "TRUE"),
new NameValuePair("x_relay_response", "False"),
new NameValuePair("x_Login", login),
new NameValuePair("x_Tran_Key", transaction),
new NameValuePair("x_Amount", amount.toString()),
new NameValuePair("x_bank_aba_code", aba),
new NameValuePair("x_bank_acct_num", account),
new NameValuePair("x_bank_acct_type", type.intValue() == 1 ?
"CHECKING" : "SAVINGS"),
new NameValuePair("x_bank_name", bank),
new NameValuePair("x_bank_acct_name", name),
new NameValuePair("x_Type", "AUTH_CAPTURE"),
new NameValuePair("x_Test_Request", test ? "TRUE" : "FALSE"),
new NameValuePair("x_currency_code", currencyCode)
};
return data;
}
private NameValuePair[] getACHRefundData(String login,
String transaction, boolean test, BigDecimal amount, String aba,
String account,
Integer type, String bank, String name,String transactionId) {
NameValuePair[] data = {
new NameValuePair("x_Version", "3.1"),
new NameValuePair("x_Delim_Data", "TRUE"),
new NameValuePair("x_relay_response", "False"),
new NameValuePair("x_Login", login),
new NameValuePair("x_Tran_Key", transaction),
new NameValuePair("x_Amount", amount.toString()),
new NameValuePair("x_bank_aba_code", aba),
new NameValuePair("x_bank_acct_num", account),
new NameValuePair("x_bank_acct_type", type.intValue() == 1 ?
"CHECKING" : "SAVINGS"),
new NameValuePair("x_bank_name", bank),
new NameValuePair("x_bank_acct_name", name),
new NameValuePair("x_Type", "CREDIT"),
new NameValuePair("x_Test_Request", test ? "TRUE" : "FALSE"),
new NameValuePair("x_Trans_ID", transactionId)
};
return data;
}
private NameValuePair[] addAVSFields(Integer userId, NameValuePair[] fields) {
try {
List result = new ArrayList();
for (int f = 0; f < fields.length; f++) {
result.add(fields[f]);
}
ContactBL contact = new ContactBL();
contact.set(userId);
considerField(result, contact.getEntity().getFirstName(),
"x_first_name");
considerField(result, contact.getEntity().getLastName(),
"x_last_name");
considerField(result, contact.getEntity().getAddress1(),
"x_address");
considerField(result, contact.getEntity().getCity(),
"x_city");
considerField(result, contact.getEntity().getStateProvince(),
"x_state");
considerField(result, contact.getEntity().getPostalCode(),
"x_zip");
considerField(result, contact.getEntity().getCountryCode(),
"x_country");
NameValuePair[] retValue = new NameValuePair[result.size()];
retValue = (NameValuePair[]) result.toArray(retValue);
return retValue;
} catch (Exception e) {
LOG.warn("Exception when trying to add the AVS fields", e);
return fields;
}
}
private void considerField(List fields, String dbField,
String aNetField) {
if (dbField != null && dbField.length() > 0) {
NameValuePair field = new NameValuePair(aNetField, dbField);
fields.add(field);
}
}
public AuthorizeNetResponseDTO makeCall(NameValuePair[] data)
throws HttpException, IOException {
Credentials creds = null;
// creds = new UsernamePasswordCredentials(args[1], args[2]);
//create a singular HttpClient object
HttpClient client = new HttpClient();
client.setConnectionTimeout(timeOut);
/*
for (int f = 0; f < data.length; f++) {
log.debug("Data=" + data[f].getName() + " " + data[f].getValue());
}
*/
//set the default credentials
if (creds != null) {
client.getState().setCredentials(null, null, creds);
}
PostMethod post = new PostMethod(url);
post.setRequestBody(data);
//execute the method
String responseBody = null;
client.executeMethod(post);
responseBody = post.getResponseBodyAsString();
LOG.debug("Got response:" + responseBody);
//write out the response body
AuthorizeNetResponseDTO dto = new AuthorizeNetResponseDTO(
responseBody);
//clean up the connection resources
post.releaseConnection();
post.recycle();
return dto;
}
/**
* The argument 'payment' has to have
* - currency
* - amount
* - credit card
* - the id of the existing payment row
* @return The information with the results of the pre-authorization, or null if the was
* a problem, such as the processor being unavailable
*/
public boolean preAuth(PaymentDTOEx payment)
throws PluggableTaskException {
String login = (String) parameters.get(PARAMETER_LOGIN.getName());
String transaction = (String) parameters.get(PARAMETER_TRANSACTION.getName());
if (login == null || login.length() == 0 || transaction == null ||
transaction.length() == 0) {
throw new PluggableTaskException("invalid parameters");
}
try {
CurrencyBL currencyBL = new CurrencyBL(payment.getCurrency().getId());
String currencyCode = currencyBL.getEntity().getCode();
NameValuePair data[] = getChargeData(login, transaction, false,
payment.getAmount(), payment.getCreditCard().getNumber(),
CreditCardBL.get4digitExpiry(payment.getCreditCard()),
currencyCode, false, new Integer(0));
AuthorizeNetResponseDTO response = makeCall(data);
// save this authorization into the DB
PaymentAuthorizationBL bl = new PaymentAuthorizationBL();
PaymentAuthorizationDTO authDto = new PaymentAuthorizationDTO(
response.getPaymentAuthorizationDTO());
authDto.setProcessor("Authorize.net");
bl.create(authDto, payment.getId());
// since this is just an authorization, without a related payment
// we leave it like this, no links to the payment table
payment.setAuthorization(authDto);
return false;
} catch (Exception e) {
LOG.info("error trying to pre-authorize", e);
return true;
}
}
public boolean confirmPreAuth(PaymentAuthorizationDTO auth,
PaymentDTOEx paymentInfo) throws PluggableTaskException {
// TODO Auto-generated method stub
// Transaction type has to be PRIOR_AUTH_CAPTURE
// the transactio id of the original authorization has to be included
// along with the amount
return true;
}
}