module ActiveMerchant #:nodoc: module Billing #:nodoc: class WorldpayGateway < Gateway TEST_URL = 'https://secure-test.wp3.rbsworldpay.com/jsp/merchant/xml/paymentService.jsp' LIVE_URL = 'https://secure.wp3.rbsworldpay.com/jsp/merchant/xml/paymentService.jsp' self.default_currency = 'GBP' self.money_format = :cents self.supported_countries = ['HK', 'US', 'GB', 'AU'] self.supported_cardtypes = [:visa, :master, :american_express, :discover] self.homepage_url = 'http://www.worldpay.com/' self.display_name = 'WorldPay' CARD_CODES = { 'visa' => 'VISA-SSL', 'master' => 'ECMC-SSL', 'discover' => 'DISCOVER-SSL', 'american_express' => 'AMEX-SSL', } def initialize(options = {}) requires!(options, :login, :password) @options = options super end def purchase(money, payment_method, options = {}) response = MultiResponse.new response << authorize(money, payment_method, options) response << capture(money, response.authorization, :authorization_validated => true) if response.success? response end def authorize(money, payment_method, options = {}) requires!(options, :order_id) commit 'authorize', build_authorization_request(money, payment_method, options) end def capture(money, authorization, options = {}) response = MultiResponse.new response << inquire(authorization, options) unless options[:authorization_validated] response << commit('capture', build_capture_request(money, authorization, options)) if response.success? response end def void(authorization, options = {}) response = MultiResponse.new response << inquire(authorization, options) response << commit('cancel', build_void_request(authorization, options)) if response.success? response end def refund(money, authorization, options = {}) response = MultiResponse.new response << inquire(authorization, options) response << commit('refund', build_refund_request(money, authorization, options)) if response.success? response end private def inquire(authorization, options={}) commit 'inquiry', build_order_inquiry_request(authorization, options) end def build_request xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.declare! :DOCTYPE, :paymentService, :PUBLIC, "-//WorldPay//DTD WorldPay PaymentService v1//EN", "http://dtd.wp3.rbsworldpay.com/paymentService_v1.dtd" xml.tag! 'paymentService', 'version' => "1.4", 'merchantCode' => @options[:login] do yield xml end xml.target! end def build_order_modify_request(authorization) build_request do |xml| xml.tag! 'modify' do xml.tag! 'orderModification', 'orderCode' => authorization do yield xml end end end end def build_order_inquiry_request(authorization, options) build_request do |xml| xml.tag! 'inquiry' do xml.tag! 'orderInquiry', 'orderCode' => authorization end end end def build_authorization_request(money, payment_method, options) build_request do |xml| xml.tag! 'submit' do xml.tag! 'order', {'orderCode' => options[:order_id], 'installationId' => @options[:inst_id]}.reject{|_,v| !v} do xml.description(options[:description].blank? ? "Purchase" : options[:description]) add_amount(xml, money, options) if options[:order_content] xml.tag! 'orderContent' do xml.cdata! options[:order_content] end end add_payment_method(xml, money, payment_method, options) end end end end def build_capture_request(money, authorization, options) build_order_modify_request(authorization) do |xml| xml.tag! 'capture' do time = Time.now xml.tag! 'date', 'dayOfMonth' => time.day, 'month' => time.month, 'year'=> time.year add_amount(xml, money, options) end end end def build_void_request(authorization, options) build_order_modify_request(authorization) do |xml| xml.tag! 'cancel' end end def build_refund_request(money, authorization, options) build_order_modify_request(authorization) do |xml| xml.tag! 'refund' do add_amount(xml, money, options) end end end def add_amount(xml, money, options) xml.tag! 'amount', :value => amount(money), 'currencyCode' => (options[:currency] || currency(money)), 'exponent' => 2 end def add_payment_method(xml, amount, payment_method, options) if payment_method.is_a?(String) xml.tag! 'payAsOrder', 'orderCode' => payment_method do add_amount(xml, amount, options) end else xml.tag! 'paymentDetails' do xml.tag! CARD_CODES[card_brand(payment_method)] do xml.tag! 'cardNumber', payment_method.number xml.tag! 'expiryDate' do xml.tag! 'date', 'month' => format(payment_method.month, :two_digits), 'year' => format(payment_method.year, :four_digits) end xml.tag! 'cardHolderName', payment_method.name xml.tag! 'cvc', payment_method.verification_value add_address(xml, 'cardAddress', (options[:billing_address] || options[:address])) end end end end def add_address(xml, element, address) return if address.nil? xml.tag! element do xml.tag! 'address' do if m = /^\s*([^\s]+)\s+(.+)$/.match(address[:name]) xml.tag! 'firstName', m[1] xml.tag! 'lastName', m[2] end if m = /^\s*(\d+)\s+(.+)$/.match(address[:address1]) xml.tag! 'street', m[2] house_number = m[1] else xml.tag! 'street', address[:address1] end xml.tag! 'houseName', address[:address2] if address[:address2] xml.tag! 'houseNumber', house_number if house_number.present? xml.tag! 'postalCode', (address[:zip].present? ? address[:zip] : "0000") xml.tag! 'city', address[:city] if address[:city] xml.tag! 'state', (address[:state].present? ? address[:state] : 'N/A') xml.tag! 'countryCode', address[:country] xml.tag! 'telephoneNumber', address[:phone] if address[:phone] end end end def parse(action, xml) parse_element({:action => action}, REXML::Document.new(xml)) end def parse_element(raw, node) node.attributes.each do |k, v| raw["#{node.name.underscore}_#{k.underscore}".to_sym] = v end if node.has_elements? raw[node.name.underscore.to_sym] = true unless node.name.blank? node.elements.each{|e| parse_element(raw, e) } else raw[node.name.underscore.to_sym] = node.text unless node.text.nil? end raw end def commit(action, request) xmr = ssl_post((test? ? TEST_URL : LIVE_URL), request, 'Content-Type' => 'text/xml', 'Authorization' => encoded_credentials) raw = parse(action, xmr) Response.new( success_from(raw), message_from(raw), raw, :authorization => authorization_from(raw), :test => test?) rescue ActiveMerchant::ResponseError => e if e.response.code.to_s == "401" return Response.new(false, "Invalid credentials", {}, :test => test?) else raise e end end def success_from(raw) (raw[:last_event] == "AUTHORISED" || raw[:ok].present?) end def message_from(raw) (raw[:iso8583_return_code_description] || raw[:error] || "SUCCESS") end def authorization_from(raw) pair = raw.detect{|k,v| k.to_s =~ /_order_code$/} (pair ? pair.last : nil) end def encoded_credentials credentials = "#{@options[:login]}:#{@options[:password]}" "Basic #{[credentials].pack('m').strip}" end class MultiResponse < Response attr_reader :responses def initialize @responses = [] end def <<(response) if response.is_a?(MultiResponse) response.responses.each{|r| @responses << r} else @responses << response end end def success? @responses.all?{|r| r.success?} end %w(params message test authorization avs_result cvv_result test? fraud_review?).each do |m| class_eval %( def #{m} @responses.last.#{m} end ) end end end end end