/* 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 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.PaymentTaskWithTimeout; import com.sapienter.jbilling.server.pluggableTask.admin.PluggableTaskException; import com.sapienter.jbilling.server.util.Constants; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.commons.httpclient.util.ParameterParser; import org.apache.log4j.Logger; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.SimpleDateFormat; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** * Abstract base class that contains all the common functionality needed to make a payment * to an RBS World Pay payment gateway. * * @author Brian Cowdery * @since 20-10-2009 */ public abstract class PaymentWorldPayBaseTask extends PaymentTaskWithTimeout { private static final Logger LOG = Logger.getLogger(PaymentWorldPayBaseTask.class); /** * Parameters for RBS WorldPay payment gateway requests */ public interface WorldPayParams { interface CreditCard { public static final String CARD_NUMBER = "CardNumber"; public static final String EXPIRATION_DATE = "ExpirationDate"; // mm/yy or mm/yyyy public static final String CVV2 = "CVV2"; // optional CVV or CVC value } interface ReAuthorize { public static final String ORDER_ID = "OrderID"; // order id returned from a previously } // successful transaction interface ForceParams { public static final String APPROVAL_CODE = "ApprovalCode"; } interface SettleParams { public static final String ORDER_ID = "OrderID"; // order number of the transaction } /** * common parameters for ACH and credit card payment */ interface General { public static final String SVC_TYPE = "SvcType"; // @see SvcType public static final String FIRST_NAME = "FirstName"; public static final String LAST_NAME = "LastName"; public static final String STREET_ADDRESS = "StreetAddress"; public static final String CITY = "City"; public static final String STATE = "State"; public static final String ZIP = "Zip"; public static final String COUNTRY = "Country"; public static final String AMOUNT = "Amount"; } } /** * RBS WorldPay gateway response parameters */ public interface WorldPayResponse { public static final String TRANSACTION_STATUS = "TransactionStatus"; // @see TransactionStatus /* transaction order number, which may be stored and used for subsequent payments through a re-authorization transaction. */ public static final String ORDER_ID = "OrderId"; /* approval codes returned by the Issuer if the authorization was approved */ public static final String APPROVAL_CODE = "ApprovalCode"; public static final String AVS_RESPONSE = "AVSResponse"; // Address Verification Service public static final String CVV2_RESPONSE = "CVV2Response"; // returned if CVV2 value was set public static final String ERROR_MSG = "ErrorMsg"; public static final String ERROR_CODE = "ErrorCode"; } /** * Represents the transaction type supported by the RBS WorldPay gateway. * * Please see <em>Appendix H: SVCTYPE</em> of the <em>API Specification - RBS WorldPay * Internet Processing Message Format</em> document. */ public enum SvcType { AUTHORIZE ("Authorize"), RE_AUTHORIZE ("ReAuthorize"), SALE ("Sale"), SETTLE ("Settle"), FORCE ("ForceSettle"), PARTIAL_SETTLE ("PartialSettle"), REFUND_ORDER ("CreditOrder"), REFUND_CREDIT ("Credit"); private String code; SvcType(String code) { this.code = code; } public String getCode() { return code; } } /** * Represents transaction status codes returned by the RBS WorldPay gateway. * * Please see <em>Appendix K: Transaction Status</em> of the <em>API Specification - RBS WorldPay * Internet Processing Message Format</em> document. */ public enum TransactionStatus { APPROVED ("0"), NOT_APPROVED ("1"), EXCEPTION ("2"); private String code; TransactionStatus(String code) { this.code = code; } public String getCode() { return code; } } /** * Class for encapsulating authorization responses */ public class WorldPayAuthorization { private final PaymentAuthorizationDTO paymentAuthDTO; public WorldPayAuthorization(String gatewayResponse) { LOG.debug("Payment authorization result of " + getProcessorName() + " gateway parsing...."); WorldPayResponseParser responseParser = new WorldPayResponseParser(gatewayResponse); paymentAuthDTO = new PaymentAuthorizationDTO(); paymentAuthDTO.setProcessor(getProcessorName()); String approvalCode = responseParser.getValue(WorldPayResponse.APPROVAL_CODE); if (approvalCode != null) { paymentAuthDTO.setApprovalCode(approvalCode); LOG.debug("approvalCode [" + paymentAuthDTO.getApprovalCode() + "]"); } String transactionStatus = responseParser.getValue(WorldPayResponse.TRANSACTION_STATUS); if (transactionStatus != null) { paymentAuthDTO.setCode2(transactionStatus); LOG.debug("transactionStatus [" + paymentAuthDTO.getCode2() + "]"); } String orderID = responseParser.getValue(WorldPayResponse.ORDER_ID); if (orderID != null) { paymentAuthDTO.setTransactionId(orderID); paymentAuthDTO.setCode1(orderID); LOG.debug("transactionID/OrderID [" + paymentAuthDTO.getTransactionId() + "]"); } String errorMsg = responseParser.getValue(WorldPayResponse.ERROR_MSG); if (errorMsg != null) { paymentAuthDTO.setResponseMessage(errorMsg); LOG.debug("errorMessage [" + paymentAuthDTO.getResponseMessage() + "]"); } } public PaymentAuthorizationDTO getDTO() { return paymentAuthDTO; } public Integer getJBResultId() { Integer resultId = Constants.RESULT_UNAVAILABLE; if (TransactionStatus.APPROVED.getCode().equals(paymentAuthDTO.getCode2())) resultId = Constants.RESULT_OK; if (TransactionStatus.NOT_APPROVED.getCode().equals(paymentAuthDTO.getCode2())) resultId = Constants.RESULT_FAIL; if (TransactionStatus.EXCEPTION.getCode().equals(paymentAuthDTO.getCode2())) resultId = Constants.RESULT_UNAVAILABLE; return resultId; } public boolean isCommunicationProblem() { return TransactionStatus.EXCEPTION.getCode().equals(paymentAuthDTO.getCode2()); } } /** * Class for gateway response parsing */ private class WorldPayResponseParser { private final String gatewayResponse; private List<NameValuePair> responseEntries; WorldPayResponseParser(String gatewayResponse) { this.gatewayResponse = gatewayResponse; parseResponse(); } public String getGatewayResponse() { return gatewayResponse; } public List<NameValuePair> getResponseEntries() { return responseEntries; } public String getValue(String responseParamName) { String val = null; for (NameValuePair pair : responseEntries) { if (pair.getName().equals(responseParamName)) { val = pair.getValue(); break; } } return val; } @SuppressWarnings("unchecked") private void parseResponse() { ParameterParser parser = new ParameterParser(); responseEntries = parser.parse(gatewayResponse, '&'); } } /** * Name value pair list to hold request parameters, to be used in conjunction with the * {@link PaymentWorldPayBaseTask#buildRequest(PaymentDTOEx, SvcType)} method as a request * builder object. */ public static class NVPList { List<NameValuePair> pairs = new LinkedList<NameValuePair>(); public void add(String name, String value) { pairs.add(new NameValuePair(name, value)); } public NameValuePair[] toArray() { return pairs.toArray(new NameValuePair[pairs.size()]); } @Override public String toString() { StringBuffer sb = new StringBuffer(); for (Iterator<NameValuePair> it = pairs.iterator(); it.hasNext();) { NameValuePair pair = it.next(); sb.append(pair.getName()) .append("=") .append(pair.getValue()); if (it.hasNext()) sb.append("&"); } return sb.toString(); } } public static final SimpleDateFormat EXPIRATION_DATE_FORMAT = new SimpleDateFormat("MM/yyyy"); /* required */ public static final String PARAMETER_STORE_ID = "STOREID"; // store id assigned by RBS World Pay public static final String PARAMETER_MERCHANT_ID = "MERCHANTID"; // merchant id assigned by RBS World Pay public static final String PARAMETER_TERMINAL_ID = "TERMINALID"; // terminal id assigned by RBS World Pay /* optional */ public static final String PARAMETER_WORLD_PAY_URL = "URL"; public static final String DEFAULT_WORLD_PAY_URL = "https://tpdev.lynksystems.com/servlet/LynkePmtServlet"; /* * optional Store SellerId associated with the StoreId. The SellerId is * mandatory if the security flag is turned on for the store. */ public static final String PARAMETER_SELLER_ID = "SELLERID"; /* * optional Password associated with the SellerId. The Password is * mandatory if the security flag is turned on for the store. */ public static final String PARAMETER_PASSWORD = "PASSWORD"; private String url; private String merchantId; private String storeId; private String terminalId; private String sellerId; private String password; public String getGatewayUrl() { if (url == null) url = getOptionalParameter(PARAMETER_WORLD_PAY_URL, DEFAULT_WORLD_PAY_URL); return url; } public String getMerchantId() throws PluggableTaskException { if (merchantId == null) merchantId = ensureGetParameter(PARAMETER_MERCHANT_ID); return merchantId; } public String getStoreId() throws PluggableTaskException { if (storeId == null) storeId = ensureGetParameter(PARAMETER_STORE_ID); return storeId; } public String getTerminalId() throws PluggableTaskException { if (terminalId == null) terminalId = ensureGetParameter(PARAMETER_TERMINAL_ID); return terminalId; } public String getSellerId() { if (sellerId == null) sellerId = getOptionalParameter(PARAMETER_SELLER_ID, ""); return sellerId; } public String getPassword() { if (password == null) password = getOptionalParameter(PARAMETER_PASSWORD, ""); return password; } /** * Utility method to format the given dollar float value to a two * digit number in compliance with the RBS WorldPay gateway API. * * @param amount dollar float value to format * @return formatted amount as a string */ public static String formatDollarAmount(BigDecimal amount) { amount = amount.abs().setScale(2, RoundingMode.HALF_EVEN); // gateway format, do not change! return amount.toPlainString(); } /** * Utility method to check if a given {@link PaymentDTOEx} payment can be processed * by this task. * * @param payment payment to check * @return true if payment can be processed with this task, false if not */ public static boolean isApplicable(PaymentDTOEx payment) { if (payment.getCreditCard() == null && payment.getAch() == null) { LOG.warn("Can't process without a credit card or ach"); return false; } return true; } /** * Returns the name of this payment processor. * @return payment processor name */ abstract String getProcessorName(); /** * Constructs a request of NameValuePairs for submission to the configured RBS WorldPay gateway, and * returns the NVPList object. * * @param payment payment to build the request for * @param transaction transaction type * @return request parameter name value pair list * @throws PluggableTaskException if an unrecoverable exception occurs */ abstract NVPList buildRequest(PaymentDTOEx payment, SvcType transaction) throws PluggableTaskException; /** * Process a payment as per the given transaction SvcType. This method relies on the abstract * {@link #buildRequest(PaymentDTOEx, SvcType)} method to build the appropriate set of HTTP request * parameters for the required transaction/process. * * @param payment payment to process * @param transaction transaction type * @param auth payment pre-authorization, may be null. * @return payment result * @throws PluggableTaskException thrown if payment instrument is not a credit card, or if a refund is attempted with no authorization */ protected Result doProcess(PaymentDTOEx payment, SvcType transaction, PaymentAuthorizationDTO auth) throws PluggableTaskException { if (!isApplicable(payment)) return NOT_APPLICABLE; if (payment.getCreditCard() == null) { LOG.error("Can't process without a credit card"); throw new PluggableTaskException("Credit card not present in payment"); } if (payment.getAch() != null) { LOG.error("Can't process with a cheque"); throw new PluggableTaskException("Can't process ACH charge"); } NVPList request = buildRequest(payment, transaction); if (auth != null && !SvcType.RE_AUTHORIZE.equals(transaction)) { // add approvalCode & orderID parameters for this settlement transaction request.add(WorldPayParams.ForceParams.APPROVAL_CODE, auth.getApprovalCode()); request.add(WorldPayParams.SettleParams.ORDER_ID, auth.getTransactionId()); } if (payment.getIsRefund() == 1 && (payment.getPayment() == null || payment.getPayment().getAuthorization() == null)) { LOG.error("Can't process refund without a payment with an authorization record"); throw new PluggableTaskException("Refund without previous authorization"); } try { LOG.debug("Processing " + transaction + " for credit card"); WorldPayAuthorization wrapper = new WorldPayAuthorization(post(request)); payment.setPaymentResult(new PaymentResultDAS().find(wrapper.getJBResultId())); // if transaction successful store it if (wrapper.getJBResultId().equals(Constants.RESULT_OK)) 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; } } /** * Make request to the configured RBS WorldPay gateway. This method returns the response * body of an HTTP post request which can be parsed using the nested {@link WorldPayResponseParser} * class. * * @param request name value pair list of request parameters * @return response from gateway * @throws IOException thrown by {@link HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)} */ protected String post(NVPList request) throws IOException { LOG.debug("Making POST request to " + getProcessorName() + " gateway ..."); HttpClient client = new HttpClient(); client.setConnectionTimeout(getTimeoutSeconds() * 1000); // todo: remove deprecated connection timeout PostMethod post = new PostMethod(getGatewayUrl()); post.setRequestEntity(new StringRequestEntity(request.toString())); LOG.debug("request body string: " + request.toString()); // execute the method client.executeMethod(post); String responseBody = post.getResponseBodyAsString(); LOG.debug("Got response:" + responseBody); // clean up the connection resources post.releaseConnection(); return responseBody; } }