require "nokogiri" require "cgi" module ActiveMerchant #:nodoc: module Billing #:nodoc: class EwayRapidGateway < Gateway self.test_url = "https://api.sandbox.ewaypayments.com/" self.live_url = "https://api.ewaypayments.com/" self.money_format = :cents self.supported_countries = ["AU"] self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] self.homepage_url = "http://www.eway.com.au/" self.display_name = "eWAY Rapid 3.0" self.default_currency = "AUD" def initialize(options = {}) requires!(options, :login, :password) super end # Public: Run a purchase transaction. Treats the Rapid 3.0 transparent # redirect as an API endpoint in order to conform to the standard # ActiveMerchant #purchase API. # # amount - The monetary amount of the transaction in cents. # options - A standard ActiveMerchant options hash: # :order_id - A merchant-supplied identifier for the # transaction (optional). # :description - A merchant-supplied description of the # transaction (optional). # :currency - Three letter currency code for the # transaction (default: "AUD") # :billing_address - Standard ActiveMerchant address hash # (optional). # :shipping_address - Standard ActiveMerchant address hash # (optional). # :ip - The ip of the consumer initiating the # transaction (optional). # :application_id - A string identifying the application # submitting the transaction # (default: "https://github.com/Shopify/active_merchant") # # Returns an ActiveMerchant::Billing::Response object def purchase(amount, payment_method, options={}) MultiResponse.new.tap do |r| # Rather than follow the redirect, we detect the 302 and capture the # token out of the Location header in the run_purchase step. But we # still need a placeholder url to pass to eWay, and that is what # example.com is used for here. r.process{setup_purchase(amount, options.merge(:redirect_url => "http://example.com/"))} r.process{run_purchase(r.authorization, payment_method, r.params["formactionurl"])} r.process{status(r.authorization)} end end # Public: Acquire the token necessary to run a transparent redirect. # # amount - The monetary amount of the transaction in cents. # options - A supplemented ActiveMerchant options hash: # :redirect_url - The url to return the customer to after # the transparent redirect is completed # (required). # :order_id - A merchant-supplied identifier for the # transaction (optional). # :description - A merchant-supplied description of the # transaction (optional). # :currency - Three letter currency code for the # transaction (default: "AUD") # :billing_address - Standard ActiveMerchant address hash # (optional). # :shipping_address - Standard ActiveMerchant address hash # (optional). # :ip - The ip of the consumer initiating the # transaction (optional). # :application_id - A string identifying the application # submitting the transaction # (default: "https://github.com/Shopify/active_merchant") # # Returns an EwayRapidResponse object, which conforms to the # ActiveMerchant::Billing::Response API, but also exposes #form_url. def setup_purchase(amount, options={}) requires!(options, :redirect_url) request = build_xml_request("CreateAccessCodeRequest") do |doc| add_metadata(doc, options) add_invoice(doc, amount, options) add_customer_data(doc, options) end commit(url_for("CreateAccessCode"), request) end # Public: Retrieve the status of a transaction. # # identification - The Eway Rapid 3.0 access code for the transaction # (returned as the response.authorization by # #setup_purchase). # # Returns an EwayRapidResponse object. def status(identification) request = build_xml_request("GetAccessCodeResultRequest") do |doc| doc.AccessCode identification end commit(url_for("GetAccessCodeResult"), request) end # Public: Store card details and return a valid token # # options - A supplemented ActiveMerchant options hash: # :order_id - A merchant-supplied identifier for the # transaction (optional). # :billing_address - Standard ActiveMerchant address hash # (required). # :ip - The ip of the consumer initiating the # transaction (optional). # :application_id - A string identifying the application # submitting the transaction # (default: "https://github.com/Shopify/active_merchant") def store(payment_method, options = {}) requires!(options, :billing_address) purchase(0, payment_method, options.merge(:request_method => "CreateTokenCustomer")) end private def run_purchase(identification, payment_method, endpoint) post = { "accesscode" => identification } add_credit_card(post, payment_method) commit_form(endpoint, build_form_request(post)) end def add_metadata(doc, options) doc.RedirectUrl(options[:redirect_url]) doc.CustomerIP options[:ip] if options[:ip] doc.Method options[:request_method] || "ProcessPayment" doc.DeviceID(options[:application_id] || application_id) end def add_invoice(doc, money, options) doc.Payment do doc.TotalAmount amount(money) doc.InvoiceReference options[:order_id] doc.InvoiceDescription options[:description] currency_code = (options[:currency] || currency(money) || default_currency) doc.CurrencyCode currency_code end end def add_customer_data(doc, options) doc.Customer do add_address(doc, (options[:billing_address] || options[:address]), {:email => options[:email]}) end doc.ShippingAddress do add_address(doc, options[:shipping_address], {:skip_company => true}) end end def add_address(doc, address, options={}) return unless address if name = address[:name] parts = name.split(/\s+/) doc.FirstName parts.shift if parts.size > 1 doc.LastName parts.join(" ") end doc.Title address[:title] doc.CompanyName address[:company] unless options[:skip_company] doc.Street1 address[:address1] doc.Street2 address[:address2] doc.City address[:city] doc.State address[:state] doc.PostalCode address[:zip] doc.Country address[:country].to_s.downcase doc.Phone address[:phone] doc.Fax address[:fax] doc.Email options[:email] end def add_credit_card(post, credit_card) post["cardname"] = credit_card.name post["cardnumber"] = credit_card.number post["cardexpirymonth"] = credit_card.month post["cardexpiryyear"] = credit_card.year post["cardcvn"] = credit_card.verification_value end def build_xml_request(root) builder = Nokogiri::XML::Builder.new builder.__send__(root) do |doc| yield(doc) end builder.to_xml end def build_form_request(post) request = [] post.each do |key, value| request << "EWAY_#{key.upcase}=#{CGI.escape(value.to_s)}" end request.join("&") end def url_for(action) (test? ? test_url : live_url) + action + ".xml" end def commit(url, request, form_post=false) headers = { "Authorization" => ("Basic " + Base64.strict_encode64(@options[:login].to_s + ":" + @options[:password].to_s).chomp), "Content-Type" => "text/xml" } raw = parse(ssl_post(url, request, headers)) succeeded = success?(raw) EwayRapidResponse.new( succeeded, message_from(succeeded, raw), raw, :authorization => authorization_from(raw), :test => test?, :avs_result => avs_result_from(raw), :cvv_result => cvv_result_from(raw) ) rescue ActiveMerchant::ResponseError => e return EwayRapidResponse.new(false, e.response.message, {:status_code => e.response.code}, :test => test?) end def commit_form(url, request) http_response = raw_ssl_request(:post, url, request) success = (http_response.code.to_s == "302") message = (success ? "Succeeded" : http_response.body) if success authorization = CGI.unescape(http_response["Location"].split("=").last) end Response.new(success, message, {:location => http_response["Location"]}, :authorization => authorization, :test => test?) end def parse(xml) response = {} doc = Nokogiri::XML(xml) doc.root.xpath("*").each do |node| if (node.elements.size == 0) response[node.name.downcase.to_sym] = node.text else node.elements.each do |childnode| name = "#{node.name.downcase}_#{childnode.name.downcase}" response[name.to_sym] = childnode.text end end end unless doc.root.nil? response end def success?(response) if response[:errors] false elsif response[:responsecode] == "00" true elsif response[:transactionstatus] (response[:transactionstatus] == "true") else true end end def message_from(succeeded, response) if response[:errors] response[:errors] elsif response[:responsecode] ActiveMerchant::Billing::EwayGateway::MESSAGES[response[:responsecode]] elsif response[:responsemessage] response[:responsemessage] elsif succeeded "Succeeded" else "Failed" end end def authorization_from(response) response[:accesscode] end def avs_result_from(response) code = case response[:verification_address] when "Valid" "M" when "Invalid" "N" else "I" end {:code => code} end def cvv_result_from(response) case response[:verification_cvn] when "Valid" "M" when "Invalid" "N" else "P" end end class EwayRapidResponse < ActiveMerchant::Billing::Response def form_url params["formactionurl"] end end end end end