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), :identification => identification) 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, parameters) http_response = raw_ssl_request(:post, url, request) success = (http_response.code.to_s == "302") message = (success ? "Succeeded" : http_response.body) authorization = parameters[:identification] if success 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] (MESSAGES[response[:errors]] || response[:errors]) elsif response[:responsecode] ActiveMerchant::Billing::EwayGateway::MESSAGES[response[:responsecode]] elsif response[:responsemessage] (MESSAGES[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 MESSAGES = { 'V6000' => 'Validation error', 'V6001' => 'Invalid CustomerIP', 'V6002' => 'Invalid DeviceID', 'V6011' => 'Invalid Payment TotalAmount', 'V6012' => 'Invalid Payment InvoiceDescription', 'V6013' => 'Invalid Payment InvoiceNumber', 'V6014' => 'Invalid Payment InvoiceReference', 'V6015' => 'Invalid Payment CurrencyCode', 'V6016' => 'Payment Required', 'V6017' => 'Payment CurrencyCode Required', 'V6018' => 'Unknown Payment CurrencyCode', 'V6021' => 'EWAY_CARDHOLDERNAME Required', 'V6022' => 'EWAY_CARDNUMBER Required', 'V6023' => 'EWAY_CARDCVN Required', 'V6033' => 'Invalid Expiry Date', 'V6034' => 'Invalid Issue Number', 'V6035' => 'Invalid Valid From Date', 'V6040' => 'Invalid TokenCustomerID', 'V6041' => 'Customer Required', 'V6042' => 'Customer FirstName Required', 'V6043' => 'Customer LastName Required', 'V6044' => 'Customer CountryCode Required', 'V6045' => 'Customer Title Required', 'V6046' => 'TokenCustomerID Required', 'V6047' => 'RedirectURL Required', 'V6051' => 'Invalid Customer FirstName', 'V6052' => 'Invalid Customer LastName', 'V6053' => 'Invalid Customer CountryCode', 'V6058' => 'Invalid Customer Title', 'V6059' => 'Invalid RedirectURL', 'V6060' => 'Invalid TokenCustomerID', 'V6061' => 'Invalid Customer Reference', 'V6062' => 'Invalid Customer CompanyName', 'V6063' => 'Invalid Customer JobDescription', 'V6064' => 'Invalid Customer Street1', 'V6065' => 'Invalid Customer Street2', 'V6066' => 'Invalid Customer City', 'V6067' => 'Invalid Customer State', 'V6068' => 'Invalid Customer PostalCode', 'V6069' => 'Invalid Customer Email', 'V6070' => 'Invalid Customer Phone', 'V6071' => 'Invalid Customer Mobile', 'V6072' => 'Invalid Customer Comments', 'V6073' => 'Invalid Customer Fax', 'V6074' => 'Invalid Customer URL', 'V6075' => 'Invalid ShippingAddress FirstName', 'V6076' => 'Invalid ShippingAddress LastName', 'V6077' => 'Invalid ShippingAddress Street1', 'V6078' => 'Invalid ShippingAddress Street2', 'V6079' => 'Invalid ShippingAddress City', 'V6080' => 'Invalid ShippingAddress State', 'V6081' => 'Invalid ShippingAddress PostalCode', 'V6082' => 'Invalid ShippingAddress Email', 'V6083' => 'Invalid ShippingAddress Phone', 'V6084' => 'Invalid ShippingAddress Country', 'V6085' => 'Invalid ShippingAddress ShippingMethod', 'V6086' => 'Invalid ShippingAddress Fax ', 'V6091' => 'Unknown Customer CountryCode', 'V6092' => 'Unknown ShippingAddress CountryCode', 'V6100' => 'Invalid EWAY_CARDNAME', 'V6101' => 'Invalid EWAY_CARDEXPIRYMONTH', 'V6102' => 'Invalid EWAY_CARDEXPIRYYEAR', 'V6103' => 'Invalid EWAY_CARDSTARTMONTH', 'V6104' => 'Invalid EWAY_CARDSTARTYEAR', 'V6105' => 'Invalid EWAY_CARDISSUENUMBER', 'V6106' => 'Invalid EWAY_CARDCVN', 'V6107' => 'Invalid EWAY_ACCESSCODE', 'V6108' => 'Invalid CustomerHostAddress', 'V6109' => 'Invalid UserAgent', 'V6110' => 'Invalid EWAY_CARDNUMBER' } end end end