require 'base64' module ActiveMerchant #:nodoc: module Billing #:nodoc: class WirecardGateway < Gateway # Test server location self.test_url = 'https://c3-test.wirecard.com/secure/ssl-gateway' # Live server location self.live_url = 'https://c3.wirecard.com/secure/ssl-gateway' # The Namespaces are not really needed, because it just tells the System, that there's actually no namespace used. # It's just specified here for completeness. ENVELOPE_NAMESPACES = { 'xmlns:xsi' => 'http://www.w3.org/1999/XMLSchema-instance', 'xsi:noNamespaceSchemaLocation' => 'wirecard.xsd' } PERMITTED_TRANSACTIONS = %w[ AUTHORIZATION CAPTURE_AUTHORIZATION PURCHASE ] RETURN_CODES = %w[ ACK NOK ] # Wirecard only allows phone numbers with a format like this: +xxx(yyy)zzz-zzzz-ppp, where: # xxx = Country code # yyy = Area or city code # zzz-zzzz = Local number # ppp = PBX extension # For example, a typical U.S. or Canadian number would be "+1(202)555-1234-739" indicating PBX extension 739 at phone # number 5551234 within area code 202 (country code 1). VALID_PHONE_FORMAT = /\+\d{1,3}(\(?\d{3}\)?)?\d{3}-\d{4}-\d{3}/ # The countries the gateway supports merchants from as 2 digit ISO country codes self.supported_countries = ['DE'] # Wirecard supports all major credit and debit cards: # Visa, Mastercard, American Express, Diners Club, # JCB, Switch, VISA Carte Bancaire, Visa Electron and UATP cards. # They also support the latest anti-fraud systems such as Verified by Visa or Master Secure Code. self.supported_cardtypes = [ :visa, :master, :american_express, :diners_club, :jcb, :switch ] # The homepage URL of the gateway self.homepage_url = 'http://www.wirecard.com' # The name of the gateway self.display_name = 'Wirecard' # The currency should normally be EUROs self.default_currency = 'EUR' # 100 is 1.00 Euro self.money_format = :cents def initialize(options = {}) # verify that username and password are supplied requires!(options, :login, :password) # unfortunately Wirecard also requires a BusinessCaseSignature in the XML request requires!(options, :signature) super end # Authorization def authorize(money, creditcard, options = {}) options[:credit_card] = creditcard commit(:authorization, money, options) end # Capture Authorization def capture(money, authorization, options = {}) options[:authorization] = authorization commit(:capture_authorization, money, options) end # Purchase def purchase(money, creditcard, options = {}) options[:credit_card] = creditcard commit(:purchase, money, options) end private def prepare_options_hash(options) result = @options.merge(options) setup_address_hash!(result) result end # Create all address hash key value pairs so that # it still works if only provided with one or two of them def setup_address_hash!(options) options[:billing_address] = options[:billing_address] || options[:address] || {} options[:shipping_address] = options[:shipping_address] || {} # Include Email in address-hash from options-hash options[:billing_address][:email] = options[:email] if options[:email] end # Contact WireCard, make the XML request, and parse the # reply into a Response object def commit(action, money, options) request = build_request(action, money, options) headers = { 'Content-Type' => 'text/xml', 'Authorization' => encoded_credentials } response = parse(ssl_post(test? ? self.test_url : self.live_url, request, headers)) # Pending Status also means Acknowledged (as stated in their specification) success = response[:FunctionResult] == "ACK" || response[:FunctionResult] == "PENDING" message = response[:Message] authorization = response[:GuWID] Response.new(success, message, response, :test => test?, :authorization => authorization, :avs_result => { :code => response[:avsCode] }, :cvv_result => response[:cvCode] ) rescue ResponseError => e if e.response.code == "401" return Response.new(false, "Invalid Login") else raise end end # Generates the complete xml-message, that gets sent to the gateway def build_request(action, money, options) options = prepare_options_hash(options) options[:action] = action xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! 'WIRECARD_BXML' do xml.tag! 'W_REQUEST' do xml.tag! 'W_JOB' do xml.tag! 'JobID', '' # UserID for this transaction xml.tag! 'BusinessCaseSignature', options[:signature] || options[:login] # Create the whole rest of the message add_transaction_data(xml, money, options) end end end xml.target! end # Includes the whole transaction data (payment, creditcard, address) def add_transaction_data(xml, money, options) options[:order_id] ||= generate_unique_id xml.tag! "FNC_CC_#{options[:action].to_s.upcase}" do xml.tag! 'FunctionID', options[:description] xml.tag! 'CC_TRANSACTION' do xml.tag! 'TransactionID', options[:order_id] case options[:action] when :authorization, :purchase add_invoice(xml, money, options) add_creditcard(xml, options[:credit_card]) add_address(xml, options[:billing_address]) when :capture_authorization xml.tag! 'GuWID', options[:authorization] end end end end # Includes the payment (amount, currency, country) to the transaction-xml def add_invoice(xml, money, options) xml.tag! 'Amount', amount(money) xml.tag! 'Currency', options[:currency] || currency(money) xml.tag! 'CountryCode', options[:billing_address][:country] xml.tag! 'RECURRING_TRANSACTION' do xml.tag! 'Type', options[:recurring] || 'Single' end end # Includes the credit-card data to the transaction-xml def add_creditcard(xml, creditcard) raise "Creditcard must be supplied!" if creditcard.nil? xml.tag! 'CREDIT_CARD_DATA' do xml.tag! 'CreditCardNumber', creditcard.number xml.tag! 'CVC2', creditcard.verification_value xml.tag! 'ExpirationYear', creditcard.year xml.tag! 'ExpirationMonth', format(creditcard.month, :two_digits) xml.tag! 'CardHolderName', [creditcard.first_name, creditcard.last_name].join(' ') end end # Includes the IP address of the customer to the transaction-xml def add_customer_data(xml, options) return unless options[:ip] xml.tag! 'CONTACT_DATA' do xml.tag! 'IPAddress', options[:ip] end end # Includes the address to the transaction-xml def add_address(xml, address) return if address.nil? xml.tag! 'CORPTRUSTCENTER_DATA' do xml.tag! 'ADDRESS' do xml.tag! 'Address1', address[:address1] xml.tag! 'Address2', address[:address2] if address[:address2] xml.tag! 'City', address[:city] xml.tag! 'ZipCode', address[:zip] if address[:state] =~ /[A-Za-z]{2}/ && address[:country] =~ /^(us|ca)$/i xml.tag! 'State', address[:state].upcase end xml.tag! 'Country', address[:country] xml.tag! 'Phone', address[:phone] if address[:phone] =~ VALID_PHONE_FORMAT xml.tag! 'Email', address[:email] end end end # Read the XML message from the gateway and check if it was successful, # and also extract required return values from the response. def parse(xml) basepath = '/WIRECARD_BXML/W_RESPONSE' response = {} xml = REXML::Document.new(xml) if root = REXML::XPath.first(xml, "#{basepath}/W_JOB") parse_response(response, root) elsif root = REXML::XPath.first(xml, "//ERROR") parse_error(response, root) else response[:Message] = "No valid XML response message received. \ Propably wrong credentials supplied with HTTP header." end response end # Parse the Element which containts all important information def parse_response(response, root) status = nil # get the root element for this Transaction root.elements.to_a.each do |node| if node.name =~ /FNC_CC_/ status = REXML::XPath.first(node, "CC_TRANSACTION/PROCESSING_STATUS") end end message = "" if status if info = status.elements['Info'] message << info.text end # Get basic response information status.elements.to_a.each do |node| response[node.name.to_sym] = (node.text || '').strip end end parse_error(root, message) response[:Message] = message end # Parse a generic error response from the gateway def parse_error(root, message = "") # Get errors if available and append them to the message errors = errors_to_string(root) unless errors.strip.blank? message << ' - ' unless message.strip.blank? message << errors end message end # Parses all elements in the response and converts the information # to a single string def errors_to_string(root) # Get context error messages (can be 0..*) errors = [] REXML::XPath.each(root, "//ERROR") do |error_elem| error = {} error[:Advice] = [] error[:Message] = error_elem.elements['Message'].text error_elem.elements.each('Advice') do |advice| error[:Advice] << advice.text end errors << error end # Convert all messages to a single string string = '' errors.each do |error| string << error[:Message] error[:Advice].each_with_index do |advice, index| string << ' (' if index == 0 string << "#{index+1}. #{advice}" string << ' and ' if index < error[:Advice].size - 1 string << ')' if index == error[:Advice].size - 1 end end string end # Encode login and password in Base64 to supply as HTTP header # (for http basic authentication) def encoded_credentials credentials = [@options[:login], @options[:password]].join(':') "Basic " << Base64.encode64(credentials).strip end end end end