module ActiveMerchant #:nodoc: module Billing #:nodoc: class OptimalPaymentGateway < Gateway TEST_URL = 'https://webservices.test.optimalpayments.com/creditcardWS/CreditCardServlet/v1' LIVE_URL = 'https://webservices.optimalpayments.com/creditcardWS/CreditCardServlet/v1' # The countries the gateway supports merchants from as 2 digit ISO country codes self.supported_countries = ['CA', 'US', 'GB'] # The card types supported by the payment gateway self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :solo] # :switch? # The homepage URL of the gateway self.homepage_url = 'http://www.optimalpayments.com/' # The name of the gateway self.display_name = 'Optimal Payments' def initialize(options = {}) #requires!(options, :login, :password) @options = options super end def authorize(money, card_or_auth, options = {}) parse_card_or_auth(card_or_auth, options) commit("cc#{@stored_data}Authorize", money, options) end alias stored_authorize authorize # back-compat def purchase(money, card_or_auth, options = {}) parse_card_or_auth(card_or_auth, options) commit("cc#{@stored_data}Purchase", money, options) end alias stored_purchase purchase # back-compat def refund(money, authorization, options = {}) options[:confirmationNumber] = authorization commit('ccCredit', money, options) end def void(authorization, options = {}) options[:confirmationNumber] = authorization commit('ccAuthorizeReversal', nil, options) end def capture(money, authorization, options = {}) options[:confirmationNumber] = authorization commit('ccSettlement', money, options) end def test? super || @options[:test] end private def parse_card_or_auth(card_or_auth, options) if card_or_auth.respond_to?(:number) @credit_card = card_or_auth @stored_data = "" else options[:confirmationNumber] = card_or_auth @stored_data = "StoredData" end end def parse(body) REXML::Document.new(body || '') end def commit(action, money, post) post[:order_id] ||= 'order_id' xml = case action when 'ccAuthorize', 'ccPurchase', 'ccVerification' cc_auth_request(money, post) when 'ccCredit', 'ccSettlement' cc_post_auth_request(money, post) when 'ccStoredDataAuthorize', 'ccStoredDataPurchase' cc_stored_data_request(money, post) when 'ccAuthorizeReversal' cc_auth_reversal_request(post) #when 'ccCancelSettle', 'ccCancelCredit', 'ccCancelPayment' # cc_cancel_request(money, post) #when 'ccPayment' # cc_payment_request(money, post) #when 'ccAuthenticate' # cc_authenticate_request(money, post) else raise 'Unknown Action' end txnRequest = URI.encode(xml) response = parse(ssl_post(test? ? TEST_URL : LIVE_URL, "txnMode=#{action}&txnRequest=#{txnRequest}")) Response.new(successful?(response), message_from(response), hash_from_xml(response), :test => test?, :authorization => authorization_from(response) ) end def successful?(response) REXML::XPath.first(response, '//decision').text == 'ACCEPTED' rescue false end def message_from(response) REXML::XPath.each(response, '//detail') do |detail| if detail.is_a?(REXML::Element) && detail.elements['tag'].text == 'InternalResponseDescription' return detail.elements['value'].text end end nil end def authorization_from(response) REXML::XPath.first(response, '//confirmationNumber').text rescue nil end def hash_from_xml(response) hsh = {} %w(confirmationNumber authCode decision code description actionCode avsResponse cvdResponse txnTime duplicateFound ).each do |tag| node = REXML::XPath.first(response, "//#{tag}") hsh[tag] = node.text if node end REXML::XPath.each(response, '//detail') do |detail| next unless detail.is_a?(REXML::Element) tag = detail.elements['tag'].text value = detail.elements['value'].text hsh[tag] = value end hsh end def xml_document(root_tag) xml = Builder::XmlMarkup.new :indent => 2 xml.tag!(root_tag, schema) do yield xml end xml.target! end def cc_auth_request(money, opts) xml_document('ccAuthRequestV1') do |xml| build_merchant_account(xml, @options) xml.merchantRefNum opts[:order_id] xml.amount(money/100.0) build_card(xml, opts) build_billing_details(xml, opts) end end def cc_auth_reversal_request(opts) xml_document('ccAuthReversalRequestV1') do |xml| build_merchant_account(xml, @options) xml.confirmationNumber opts[:confirmationNumber] xml.merchantRefNum opts[:order_id] end end def cc_post_auth_request(money, opts) xml_document('ccPostAuthRequestV1') do |xml| build_merchant_account(xml, @options) xml.confirmationNumber opts[:confirmationNumber] xml.merchantRefNum opts[:order_id] xml.amount(money/100.0) end end def cc_stored_data_request(money, opts) xml_document('ccStoredDataRequestV1') do |xml| build_merchant_account(xml, @options) xml.merchantRefNum opts[:order_id] xml.confirmationNumber opts[:confirmationNumber] xml.amount(money/100.0) end end # untested # # def cc_cancel_request(opts) # xml_document('ccCancelRequestV1') do |xml| # build_merchant_account(xml, @options) # xml.confirmationNumber opts[:confirmationNumber] # end # end # # def cc_payment_request(money, opts) # xml_document('ccPaymentRequestV1') do |xml| # build_merchant_account(xml, @options) # xml.merchantRefNum opts[:order_id] # xml.amount(money/100.0) # build_card(xml, opts) # build_billing_details(xml, opts) # end # end # # def cc_authenticate_request(opts) # xml_document('ccAuthenticateRequestV1') do |xml| # build_merchant_account(xml, @options) # xml.confirmationNumber opts[:confirmationNumber] # xml.paymentResponse 'myPaymentResponse' # end # end def schema { 'xmlns' => 'http://www.optimalpayments.com/creditcard/xmlschema/v1', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.optimalpayments.com/creditcard/xmlschema/v1' } end def build_merchant_account(xml, opts) xml.tag! 'merchantAccount' do xml.tag! 'accountNum' , opts[:account] xml.tag! 'storeID' , opts[:login] xml.tag! 'storePwd' , opts[:password] end end def build_card(xml, opts) xml.tag! 'card' do xml.tag! 'cardNum' , @credit_card.number xml.tag! 'cardExpiry' do xml.tag! 'month' , @credit_card.month xml.tag! 'year' , @credit_card.year end if type = card_type(@credit_card.type) xml.tag! 'cardType' , type end if @credit_card.verification_value xml.tag! 'cvdIndicator' , '1' # Value Provided xml.tag! 'cvd' , @credit_card.verification_value end end end def build_billing_details(xml, opts) xml.tag! 'billingDetails' do addr = opts[:billing_address] xml.tag! 'cardPayMethod', 'WEB' if addr[:name] xml.tag! 'firstName', CGI.escape(addr[:name].split(' ').first) # TODO: parse properly xml.tag! 'lastName' , CGI.escape(addr[:name].split(' ').last ) end xml.tag! 'street' , CGI.escape(addr[:address1]) if addr[:address1] && !addr[:address1].empty? xml.tag! 'street2', CGI.escape(addr[:address2]) if addr[:address2] && !addr[:address2].empty? xml.tag! 'city' , CGI.escape(addr[:city] ) if addr[:city] && !addr[:city].empty? xml.tag! 'state' , CGI.escape(addr[:state] ) if addr[:state] && !addr[:state].empty? xml.tag! 'country', CGI.escape(addr[:country] ) if addr[:country] && !addr[:country].empty? xml.tag! 'zip' , CGI.escape(addr[:zip] ) # this one's actually required xml.tag! 'phone' , CGI.escape(addr[:phone] ) if addr[:phone] && !addr[:phone].empty? #xml.tag! 'email' , '' end end def card_type(key) { 'visa' => 'VI', 'master' => 'MC', 'american_express'=> 'AM', 'discover' => 'DI', 'diners_club' => 'DC', #'switch' => '', 'solo' => 'SO' }[key] end end end end