/*
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.payment.tasks;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.LinkedList;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.log4j.Logger;
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.PaymentTask;
import com.sapienter.jbilling.server.pluggableTask.PaymentTaskWithTimeout;
import com.sapienter.jbilling.server.pluggableTask.admin.PluggableTaskDTO;
import com.sapienter.jbilling.server.pluggableTask.admin.PluggableTaskException;
import com.sapienter.jbilling.server.user.ContactBL;
import com.sapienter.jbilling.server.user.contact.db.ContactDTO;
import com.sapienter.jbilling.server.user.db.AchDTO;
import com.sapienter.jbilling.server.user.db.CreditCardDTO;
import com.sapienter.jbilling.server.util.Constants;
/**
* A pluggable PaymentTask that uses Sage gateway for Bankcard and ACH
* transactions.
*
* The following parameters must be configured for this payment task to work: -
*
* merchantid - merchant id provided by Sage merchantkey - merchant key provided
* by Sage
*
* timeout_sec - number of seconds for timeout (in inheritance from
* PaymentTaskWithTimeout)
*
* @author Krylenko
*/
public class PaymentSageTask extends PaymentTaskWithTimeout implements
PaymentTask {
// ------------------------ Constants -----------------------
// names of plugin parameters
private interface Params {
public static final String MERCHANT_ID = "merchantid";
public static final String MERCHANT_KEY = "merchantkey";
}
// Gateway urls
private interface Urls {
public static final String BANKCARD_POST_URL = "https://www.sagepayments.net/cgi-bin/eftBankcard.dll?transaction";
public static final String VIRTUAL_CHECK_POST_URL = "https://www.sagepayments.net/cgi-bin/eftVirtualCheck.dll?transaction";
}
// Names of gateway parameters
private interface SageParams {
// common parameters for ACH and credit card payment
interface General {
public static final String MERCHANT_ID = "M_id";
public static final String MERCHANT_KEY = "M_key";
public static final String BILLING_ADDRESS = "C_address";
public static final String BILLING_CITY = "C_city";
public static final String BILLING_STATE = "C_state";
public static final String BILLING_ZIP = "C_zip";
public static final String EMAIL = "C_email";
public static final String TRANSACTION_AMOUNT = "T_amt";
public static final String TRANSACTION_CODE = "T_code";
public static final String TRANSACTION_AUTH_CODE = "T_auth";
}
// parameters for credit card payment
interface CreditCard {
public static final String NAME = "C_name";
public static final String CARD_NUMBER = "C_cardnumber";
public static final String CARD_EXPIRATION_DATE = "C_exp";
public static final String CARD_CVV = "C_cvv";
}
// parameters for ACH payment
interface VirtualCheck {
public static final String FIRST_NAME = "C_first_name";
public static final String LAST_NAME = "C_last_name";
public static final String BILLING_COUNTRY = "C_country";
public static final String ROUTING_NUMBER = "C_rte";
public static final String BANK_ACCOUNT_NUMBER = "C_acct";
public static final String BANK_ACCOUNT_TYPE = "C_acct_type";
public static final String CUSTOMER_TYPE = "C_customer_type";
}
}
// Gateway constants
private interface SageValues {
interface BankAccountType {
public static final String CHECKING = "DDA";
public static final String SAVING = "SAV";
}
interface CustomerType {
public static final String PERSONAL_MERCHANT_INITIATED = "PPD";
}
interface ApprovalIndicator {
public static final String APPROVED = "A";
public static final String FRONT_END_ERROR = "E";
public static final String GATEWAY_ERROR = "X";
}
String[] ServerErrors = new String[] { "000000", "999999" };
}
// Type of transaction
private enum Transaction {
Payment("01"), AuthOnly("02"), Force("03"), Credit("06");
private String code;
private Transaction(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
// Payment processor identificator
private static final String PROCESSOR = "Sage";
// Credit card expiration format
private static final String DATE_FORMAT = "MMyy";
// Value for checking bank account type
private static final int CHECKING = 1;
private static final Logger LOG = Logger.getLogger(PaymentSageTask.class);
// ------------------------ Fields --------------------------
private String merchantId;
private String merchantKey;
// ------------------------ Public methods ------------------
/**
* You could initialize plugin parameters here
*/
@Override
public void initializeParamters(PluggableTaskDTO task)
throws PluggableTaskException {
super.initializeParamters(task);
merchantId = ensureGetParameter(Params.MERCHANT_ID);
merchantKey = ensureGetParameter(Params.MERCHANT_KEY);
}
/**
* Method for payment processisng
*
* @param payment
* payment data
* @return true if the next payment processor should be called by the
* business plugin manager. In other words, for result of success of
* failure, the return is false. If the communication with the
* payment processor fails (server down, timeout, etc), return
* false.
*/
public boolean process(PaymentDTOEx payment) throws PluggableTaskException {
LOG.debug("Payment processing for " + PROCESSOR + " gateway");
Transaction transaction = Transaction.Payment;
if (BigDecimal.ZERO.compareTo(payment.getAmount()) > 0) {
transaction = Transaction.Credit;
LOG.debug("Doing a credit transaction");
// note: formatAmount() will make amount positive for sending to gateway
}
boolean result = doProcess(payment, transaction, null)
.shouldCallOtherProcessors();
LOG.debug("Processing result is " + payment.getPaymentResult().getId()
+ ", return value of process is " + result);
return result;
}
/**
* Do a credit card preauthorization of a fixed amount.
*
* @param payment
* payment data
* @return see prosess method description
*/
public boolean preAuth(PaymentDTOEx payment) throws PluggableTaskException {
LOG.debug("PreAuth processing for " + PROCESSOR + " gateway");
return doProcess(payment, Transaction.AuthOnly, null)
.shouldCallOtherProcessors();
}
/**
* Take a transaction done with 'preAuth' and confirm it
*
* @param auth
* return value of preAuth.
* @param payment
* payment data
* @return see prosess method description
*/
public boolean confirmPreAuth(PaymentAuthorizationDTO auth,
PaymentDTOEx payment) throws PluggableTaskException {
LOG.debug("ConfirmPreAuth processing for " + PROCESSOR + " gateway");
if (!PROCESSOR.equals(auth.getProcessor())) {
LOG.warn("The procesor of the pre-auth is not paypal, is "
+ auth.getProcessor());
// let the processor be called and failed, so the caller
// can do something about it: probably call this one again but for
// 'process'
}
CreditCardDTO card = payment.getCreditCard();
if (card == null) {
throw new PluggableTaskException(
"Credit card is required capturing" + " payment: "
+ payment.getId());
}
if (!isApplicable(payment)) {
LOG.error("This payment can not be captured" + payment);
return true;
}
return doProcess(payment, Transaction.Force, auth)
.shouldCallOtherProcessors();
}
/**
* Method has been obsoleted
*/
public void failure(Integer userId, Integer retry) {
// ignore
}
// ------------------------ Private methods -----------------
/**
* Process transaction
*
* @param payment
* payment data
* @param transaction
* type of transaction
* @param auth
* data for confirmPreAuth operation
*/
private Result doProcess(PaymentDTOEx payment, Transaction transaction,
PaymentAuthorizationDTO auth) throws PluggableTaskException {
if (!isApplicable(payment)) {
return NOT_APPLICABLE;
}
NVPList request = new NVPList();
fillData(request, payment, transaction);
if (Transaction.Force == transaction) {
request.add(SageParams.General.TRANSACTION_AUTH_CODE, auth
.getTransactionId());
}
try {
boolean isAch = isAch(payment);
LOG.debug("Processing " + transaction + " for "
+ (isAch ? "ACH" : "credit card"));
SageAuthorization wrapper = new SageAuthorization(makeCall(request,
isAch), isAch);
payment.setPaymentResult(new PaymentResultDAS().find(wrapper.getJBResultId()));
storeProcessedAuthorization(payment, wrapper.getDTO());
return new Result(wrapper.getDTO(), wrapper
.isCommunicationProblem());
} catch (Exception e) {
LOG.error("Couldn't handle payment request due to error", e);
payment.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_UNAVAILABLE));
return NOT_APPLICABLE;
}
}
/**
* Method check if plugin could handle this operation
*
* @param payment
* payment data
*/
private boolean isApplicable(PaymentDTOEx payment) {
if (payment.getCreditCard() == null && payment.getAch() == null) {
LOG.warn("Can't process without a credit card or ach");
return false;
}
Integer refund = payment.getIsRefund();
if (refund == null || refund == 0) {
return true;
} else {
// todo: check if it true in Sage
LOG.warn("Can't process refund");
return false;
}
}
/**
* Check type of payment (ACH or credit card)
*
* @param payment
* payment data
*/
private boolean isAch(PaymentDTOEx payment) {
// we use ACH payment by default
return (payment.getAch() != null);
}
/**
* Make request to agteway
*
* @return response from gateway
*/
private String makeCall(NVPList request, boolean isAch) throws IOException {
LOG.debug("Request to " + PROCESSOR + " gateway sending...");
// create a singular HttpClient object
HttpClient client = new HttpClient();
client.setConnectionTimeout(getTimeoutSeconds() * 1000);
PostMethod post = new PostMethod(getUrl(isAch));
post.setRequestBody(request.toArray());
// execute the method
client.executeMethod(post);
String responseBody = post.getResponseBodyAsString();
LOG.debug("Got response:" + responseBody);
// clean up the connection resources
post.releaseConnection();
post.recycle();
return responseBody;
}
/**
* Fill request parameter with payment data
*/
private void fillData(NVPList request, PaymentDTOEx payment,
Transaction transaction) throws PluggableTaskException {
boolean isAch = isAch(payment);
request.add(SageParams.General.MERCHANT_ID, merchantId);
request.add(SageParams.General.MERCHANT_KEY, merchantKey);
try {
ContactBL contact = new ContactBL();
contact.set(payment.getUserId());
ContactDTO contactEntity = contact.getEntity();
request.add(SageParams.General.BILLING_ADDRESS, contactEntity
.getAddress1());
request.add(SageParams.General.BILLING_CITY, contactEntity
.getCity());
request.add(SageParams.General.BILLING_STATE, contactEntity
.getStateProvince());
request.add(SageParams.General.BILLING_ZIP, contactEntity
.getPostalCode());
if (isAch) {
request.add(SageParams.VirtualCheck.FIRST_NAME, contactEntity
.getFirstName());
request.add(SageParams.VirtualCheck.LAST_NAME, contactEntity
.getLastName());
request.add(SageParams.VirtualCheck.BILLING_COUNTRY,
contactEntity.getCountryCode());
} else {
request.add(SageParams.CreditCard.NAME, contactEntity
.getLastName()
+ " " + contactEntity.getFirstName());
}
} catch (Exception e) {
throw new PluggableTaskException(
"Error loading Contact for user id " + payment.getUserId(),
e);
}
request.add(SageParams.General.TRANSACTION_AMOUNT, formatAmount(payment.getAmount()));
request.add(SageParams.General.TRANSACTION_CODE, transaction.getCode());
if (isAch) {
AchDTO ach = payment.getAch();
request.add(SageParams.VirtualCheck.ROUTING_NUMBER, ach
.getAbaRouting());
request.add(SageParams.VirtualCheck.BANK_ACCOUNT_NUMBER, ach
.getBankAccount());
request
.add(
SageParams.VirtualCheck.BANK_ACCOUNT_NUMBER,
ach.getAccountType() == CHECKING ? SageValues.BankAccountType.CHECKING
: SageValues.BankAccountType.SAVING);
request.add(SageParams.VirtualCheck.CUSTOMER_TYPE,
SageValues.CustomerType.PERSONAL_MERCHANT_INITIATED);
} else {
CreditCardDTO card = payment.getCreditCard();
request.add(SageParams.CreditCard.CARD_NUMBER, card.getNumber());
request.add(SageParams.CreditCard.CARD_EXPIRATION_DATE,
new SimpleDateFormat(DATE_FORMAT).format(card.getCcExpiry()));
if (card.getSecurityCode() != null) {
request.add(SageParams.CreditCard.CARD_CVV, String
.valueOf(payment.getCreditCard().getSecurityCode()));
}
}
}
/**
* Format number to gateway format
*/
private String formatAmount(BigDecimal amount) {
amount = amount.abs().setScale(2, RoundingMode.HALF_EVEN); // gateway format, do not change!
return amount.toPlainString();
}
/**
* @return Gateway url
*/
private String getUrl(boolean isAch) {
return isAch ? Urls.VIRTUAL_CHECK_POST_URL : Urls.BANKCARD_POST_URL;
}
/**
* Check if it server error
*
* @param errorCode
* error code which was returned by gateway
*/
private boolean isServerError(String errorCode) {
for (String serverError : SageValues.ServerErrors) {
if (serverError.equals(errorCode))
return true;
}
return false;
}
// ------------------------ Private classes -----------------
/**
* Class for request parameters keeping
*/
private static class NVPList extends LinkedList<NameValuePair> {
public void add(String name, String value) {
add(new NameValuePair(name, value));
}
public NameValuePair[] toArray() {
return super.toArray(new NameValuePair[size()]);
}
}
/**
* Class for authorization response incaptulating
*/
private class SageAuthorization {
private final PaymentAuthorizationDTO paymentAuthDTO;
public SageAuthorization(String gatewayResponse, boolean isAch) {
LOG.debug("Payment authorization result of " + PROCESSOR
+ " gateway parsing....");
SageResponseParser responseParser = new SageResponseParser(
gatewayResponse, isAch);
paymentAuthDTO = new PaymentAuthorizationDTO();
paymentAuthDTO.setProcessor(PROCESSOR);
paymentAuthDTO.setApprovalCode(responseParser
.getValue(responseParser.approvalCode));
LOG
.debug("approvalCode [" + paymentAuthDTO.getApprovalCode()
+ "]");
paymentAuthDTO.setResponseMessage(responseParser
.getValue(responseParser.responseMessage));
LOG.debug("responseMessage [" + paymentAuthDTO.getResponseMessage()
+ "]");
paymentAuthDTO.setCode1(responseParser
.getValue(responseParser.approvalIndicator));
LOG.debug("approvalIndicator [" + paymentAuthDTO.getCode1() + "]");
paymentAuthDTO.setCode2(responseParser
.getValue(responseParser.cvvIndicator));
LOG.debug("cvvIndicator [" + paymentAuthDTO.getCode2() + "]");
paymentAuthDTO.setAvs(responseParser
.getValue(responseParser.avsIndicator));
LOG.debug("avsIndicator [" + paymentAuthDTO.getAvs() + "]");
paymentAuthDTO.setCode3(responseParser
.getValue(responseParser.riskIndicator));
LOG.debug("riskIndicator [" + paymentAuthDTO.getCode3() + "]");
paymentAuthDTO.setTransactionId(responseParser
.getValue(responseParser.reference));
LOG.debug("reference [" + paymentAuthDTO.getTransactionId() + "]");
}
public PaymentAuthorizationDTO getDTO() {
return paymentAuthDTO;
}
public Integer getJBResultId() {
if (isCommunicationProblem()) {
return Constants.RESULT_UNAVAILABLE;
}
return SageValues.ApprovalIndicator.APPROVED
.equalsIgnoreCase(paymentAuthDTO.getCode1()) ? Constants.RESULT_OK
: Constants.RESULT_FAIL;
}
public boolean isCommunicationProblem() {
return SageValues.ApprovalIndicator.GATEWAY_ERROR
.equalsIgnoreCase(paymentAuthDTO.getCode1())
&& isServerError(paymentAuthDTO.getApprovalCode());
}
}
/**
* Class for gateway's response parsing
*/
private class SageResponseParser {
SageResponseEntry approvalIndicator;
SageResponseEntry approvalCode;
SageResponseEntry responseMessage;
SageResponseEntry cvvIndicator;
SageResponseEntry avsIndicator;
SageResponseEntry riskIndicator;
SageResponseEntry reference;
private final String gatewayResponse;
SageResponseParser(String gatewayResponse, boolean isAch) {
this.gatewayResponse = gatewayResponse;
approvalIndicator = new SageResponseEntry(2, 1);
approvalCode = new SageResponseEntry(3, 6);
responseMessage = new SageResponseEntry(9, 32);
if (isAch) {
riskIndicator = new SageResponseEntry(41, 2);
reference = new SageResponseEntry(43, 10);
} else {
cvvIndicator = new SageResponseEntry(43, 1);
avsIndicator = new SageResponseEntry(44, 1);
riskIndicator = new SageResponseEntry(45, 2);
reference = new SageResponseEntry(47, 10);
}
}
String getValue(SageResponseEntry entry) {
return (null == entry) ? null : entry.getValue();
}
/**
* Class for gateway response entry incapsulating
*/
class SageResponseEntry {
private final int start;
private final int length;
private SageResponseEntry(int start, int length) {
this.start = start;
this.length = length;
}
String getValue() {
return (start - 1 > 0 && start + length - 1 < gatewayResponse
.length()) ? gatewayResponse.substring(start - 1, start
+ length - 1) : null;
}
}
}
}