module ActiveMerchant #:nodoc: module Billing #:nodoc: class FirstdataE4V27Gateway < Gateway self.test_url = 'https://api.demo.globalgatewaye4.firstdata.com/transaction/v28' self.live_url = 'https://api.globalgatewaye4.firstdata.com/transaction/v28' TRANSACTIONS = { sale: '00', authorization: '01', verify: '05', capture: '32', void: '33', credit: '34', store: '05' } SUCCESS = 'true' SENSITIVE_FIELDS = %i[cvdcode expiry_date card_number] BRANDS = { visa: 'Visa', master: 'Mastercard', american_express: 'American Express', jcb: 'JCB', discover: 'Discover' } DEFAULT_ECI = '07' self.supported_cardtypes = BRANDS.keys self.supported_countries = %w[CA US] self.default_currency = 'USD' self.homepage_url = 'http://www.firstdata.com' self.display_name = 'FirstData Global Gateway e4 v27' STANDARD_ERROR_CODE_MAPPING = { # Bank error codes: https://support.payeezy.com/hc/en-us/articles/203730509-First-Data-Global-Gateway-e4-Bank-Response-Codes '201' => STANDARD_ERROR_CODE[:incorrect_number], '531' => STANDARD_ERROR_CODE[:invalid_cvc], '503' => STANDARD_ERROR_CODE[:invalid_cvc], '811' => STANDARD_ERROR_CODE[:invalid_cvc], '605' => STANDARD_ERROR_CODE[:invalid_expiry_date], '522' => STANDARD_ERROR_CODE[:expired_card], '303' => STANDARD_ERROR_CODE[:card_declined], '530' => STANDARD_ERROR_CODE[:card_declined], '401' => STANDARD_ERROR_CODE[:call_issuer], '402' => STANDARD_ERROR_CODE[:call_issuer], '501' => STANDARD_ERROR_CODE[:pickup_card], # Ecommerce error codes: https://support.payeezy.com/hc/en-us/articles/203730499-eCommerce-Response-Codes-ETG-e4-Transaction-Gateway-Codes '22' => STANDARD_ERROR_CODE[:invalid_number], '25' => STANDARD_ERROR_CODE[:invalid_expiry_date], '31' => STANDARD_ERROR_CODE[:incorrect_cvc], '44' => STANDARD_ERROR_CODE[:incorrect_zip], '42' => STANDARD_ERROR_CODE[:processing_error] } def initialize(options = {}) requires!(options, :login, :password, :key_id, :hmac_key) @options = options super end def authorize(money, credit_card_or_store_authorization, options = {}) commit(:authorization, build_sale_or_authorization_request(money, credit_card_or_store_authorization, options)) end def purchase(money, credit_card_or_store_authorization, options = {}) commit(:sale, build_sale_or_authorization_request(money, credit_card_or_store_authorization, options)) end def capture(money, authorization, options = {}) commit(:capture, build_capture_or_credit_request(money, authorization, options)) end def void(authorization, options = {}) commit(:void, build_capture_or_credit_request(money_from_authorization(authorization), authorization, options)) end def refund(money, authorization, options = {}) commit(:credit, build_capture_or_credit_request(money, authorization, options)) end def verify(credit_card, options = {}) commit(:verify, build_sale_or_authorization_request(0, credit_card, options)) end # Tokenize a credit card with TransArmor # # The TransArmor token and other card data necessary for subsequent # transactions is stored in the response's +authorization+ attribute. # The authorization string may be passed to +authorize+ and +purchase+ # instead of a +ActiveMerchant::Billing::CreditCard+ instance. # # TransArmor support must be explicitly activated on your gateway # account by FirstData. If your authorization string is empty, contact # FirstData support for account setup assistance. # # https://support.payeezy.com/hc/en-us/articles/203731189-TransArmor-Tokenization def store(credit_card, options = {}) commit(:store, build_store_request(credit_card, options), credit_card) end def verify_credentials response = void('0') response.message != 'Unauthorized Request. Bad or missing credentials.' end def supports_scrubbing? true end def scrub(transcript) transcript. gsub(%r(().+()), '\1[FILTERED]\2'). gsub(%r(().+()), '\1[FILTERED]\2'). gsub(%r(().+())i, '\1[FILTERED]\2'). gsub(%r(().+()), '\1[FILTERED]\2'). gsub(%r((CARD NUMBER\s+: )#+\d+), '\1[FILTERED]') end def supports_network_tokenization? true end private def build_request(action, body) xml = Builder::XmlMarkup.new xml.instruct! xml.tag! 'Transaction', xmlns: 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/encodedTypes' do add_credentials(xml) add_transaction_type(xml, action) xml << body end xml.target! end def build_sale_or_authorization_request(money, credit_card_or_store_authorization, options) xml = Builder::XmlMarkup.new add_amount(xml, money, options) if credit_card_or_store_authorization.is_a? String add_credit_card_token(xml, credit_card_or_store_authorization, options) else add_credit_card(xml, credit_card_or_store_authorization, options) add_stored_credentials(xml, credit_card_or_store_authorization, options) end add_address(xml, options) add_customer_data(xml, options) add_invoice(xml, options) add_tax_fields(xml, options) add_level_3(xml, options) xml.target! end def build_capture_or_credit_request(money, identification, options) xml = Builder::XmlMarkup.new add_identification(xml, identification) add_amount(xml, money, options) add_customer_data(xml, options) add_card_authentication_data(xml, options) xml.target! end def build_store_request(credit_card, options) xml = Builder::XmlMarkup.new add_credit_card(xml, credit_card, options) add_address(xml, options) add_customer_data(xml, options) xml.target! end def add_credentials(xml) xml.tag! 'ExactID', @options[:login] xml.tag! 'Password', @options[:password] end def add_transaction_type(xml, action) xml.tag! 'Transaction_Type', TRANSACTIONS[action] end def add_identification(xml, identification) authorization_num, transaction_tag, = identification.split(';') xml.tag! 'Authorization_Num', authorization_num xml.tag! 'Transaction_Tag', transaction_tag end def add_amount(xml, money, options) currency_code = options[:currency] || default_currency xml.tag! 'DollarAmount', localized_amount(money, currency_code) xml.tag! 'Currency', currency_code end def add_credit_card(xml, credit_card, options) if credit_card.respond_to?(:track_data) && credit_card.track_data.present? xml.tag! 'Track1', credit_card.track_data xml.tag! 'Ecommerce_Flag', 'R' else xml.tag! 'Card_Number', credit_card.number xml.tag! 'Expiry_Date', expdate(credit_card) xml.tag! 'CardHoldersName', credit_card.name xml.tag! 'CardType', card_type(credit_card.brand) xml.tag! 'WalletProviderID', options[:wallet_provider_id] if options[:wallet_provider_id] add_credit_card_eci(xml, credit_card, options) add_credit_card_verification_strings(xml, credit_card, options) end end def add_credit_card_eci(xml, credit_card, options) eci = if credit_card.is_a?(NetworkTokenizationCreditCard) && credit_card.source == :apple_pay && card_brand(credit_card) == 'discover' # Discover requires any Apple Pay transaction, regardless of in-app # or web, and regardless of the ECI contained in the PKPaymentToken, # to have an ECI value explicitly of 04. '04' else (credit_card.respond_to?(:eci) ? credit_card.eci : nil) || options[:eci] || DEFAULT_ECI end xml.tag! 'Ecommerce_Flag', /^[0-9]+$/.match?(eci.to_s) ? eci.to_s.rjust(2, '0') : eci end def add_credit_card_verification_strings(xml, credit_card, options) if credit_card.is_a?(NetworkTokenizationCreditCard) add_network_tokenization_credit_card(xml, credit_card) else if credit_card.verification_value? xml.tag! 'CVD_Presence_Ind', '1' xml.tag! 'CVDCode', credit_card.verification_value end add_card_authentication_data(xml, options) end end def add_network_tokenization_credit_card(xml, credit_card) case card_brand(credit_card).to_sym when :american_express cryptogram = Base64.decode64(credit_card.payment_cryptogram) xml.tag!('XID', Base64.encode64(cryptogram[20...40])) xml.tag!('CAVV', Base64.encode64(cryptogram[0...20])) else xml.tag!('XID', credit_card.transaction_id) if credit_card.transaction_id xml.tag!('CAVV', credit_card.payment_cryptogram) end end def add_card_authentication_data(xml, options) xml.tag! 'CAVV', options[:cavv] xml.tag! 'XID', options[:xid] end def add_credit_card_token(xml, store_authorization, options) params = store_authorization.split(';') credit_card = CreditCard.new( brand: params[1], first_name: params[2], last_name: params[3], month: params[4], year: params[5] ) xml.tag! 'TransarmorToken', params[0] xml.tag! 'Expiry_Date', expdate(credit_card) xml.tag! 'CardHoldersName', credit_card.name xml.tag! 'CardType', card_type(credit_card.brand) xml.tag! 'WalletProviderID', options[:wallet_provider_id] if options[:wallet_provider_id] add_card_authentication_data(xml, options) end def add_customer_data(xml, options) xml.tag! 'Customer_Ref', options[:customer] if options[:customer] xml.tag! 'Client_IP', options[:ip] if options[:ip] xml.tag! 'Client_Email', options[:email] if options[:email] end def add_address(xml, options) if (address = options[:billing_address] || options[:address]) address = strip_line_breaks(address) xml.tag! 'Address' do xml.tag! 'Address1', address[:address1] xml.tag! 'Address2', address[:address2] if address[:address2] xml.tag! 'City', address[:city] xml.tag! 'State', address[:state] xml.tag! 'Zip', address[:zip] xml.tag! 'CountryCode', address[:country] end xml.tag! 'ZipCode', address[:zip] end end def strip_line_breaks(address) return unless address.is_a?(Hash) Hash[address.map { |k, s| [k, s&.tr("\r\n", ' ')&.strip] }] end def add_invoice(xml, options) xml.tag! 'Reference_No', options[:order_id] xml.tag! 'Reference_3', options[:description] if options[:description] end def add_tax_fields(xml, options) xml.tag! 'Tax1Amount', options[:tax1_amount] if options[:tax1_amount] xml.tag! 'Tax1Number', options[:tax1_number] if options[:tax1_number] end def add_level_3(xml, options) xml.tag!('Level3') { |x| x << options[:level_3] } if options[:level_3] end def add_stored_credentials(xml, card, options) return unless options[:stored_credential] xml.tag! 'StoredCredentials' do xml.tag! 'Indicator', stored_credential_indicator(xml, card, options) if initiator = options.dig(:stored_credential, :initiator) xml.tag! 'Initiation', initiator == 'merchant' ? 'M' : 'C' end if reason_type = options.dig(:stored_credential, :reason_type) xml.tag! 'Schedule', reason_type == 'unscheduled' ? 'U' : 'S' end xml.tag! 'AuthorizationTypeOverride', options[:authorization_type_override] if options[:authorization_type_override] if network_transaction_id = options[:stored_credential][:network_transaction_id] xml.tag! 'TransactionId', network_transaction_id else xml.tag! 'TransactionId', 'new' end xml.tag! 'OriginalAmount', options[:original_amount] if options[:original_amount] xml.tag! 'ProtectbuyIndicator', options[:protectbuy_indicator] if options[:protectbuy_indicator] end end def stored_credential_indicator(xml, card, options) if card.brand == 'master' || options.dig(:stored_credential, :initial_transaction) == false 'S' else '1' end end def expdate(credit_card) "#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}" end def card_type(credit_card_brand) BRANDS[credit_card_brand.to_sym] if credit_card_brand end def commit(action, data, credit_card = nil) url = (test? ? self.test_url : self.live_url) request = build_request(action, data) begin response = parse(ssl_post(url, request, headers('POST', url, request))) rescue ResponseError => e response = parse_error(e.response) end Response.new(successful?(response), message_from(response), response, test: test?, authorization: successful?(response) ? response_authorization(action, response, credit_card) : '', avs_result: { code: response[:avs] }, cvv_result: response[:cvv2], error_code: standard_error_code(response)) end def headers(method, url, request) content_type = 'application/xml' content_digest = Digest::SHA1.hexdigest(request) sending_time = Time.now.utc.iso8601 payload = [method, content_type, content_digest, sending_time, url.split('.com')[1]].join("\n") hmac = OpenSSL::HMAC.digest('sha1', @options[:hmac_key], payload) encoded = Base64.strict_encode64(hmac) { 'x-gge4-date' => sending_time, 'x-gge4-content-sha1' => content_digest, 'Authorization' => 'GGE4_API ' + @options[:key_id].to_s + ':' + encoded, 'Accepts' => content_type, 'Content-Type' => content_type } end def successful?(response) response[:transaction_approved] == SUCCESS end def response_authorization(action, response, credit_card) if action == :store store_authorization_from(response, credit_card) else authorization_from(response) end end def authorization_from(response) if response[:authorization_num] && response[:transaction_tag] [ response[:authorization_num], response[:transaction_tag], (response[:dollar_amount].to_f * 100).round ].join(';') else '' end end def store_authorization_from(response, credit_card) if response[:transarmor_token].present? [ response[:transarmor_token], credit_card.brand, credit_card.first_name, credit_card.last_name, credit_card.month, credit_card.year ].map { |value| value.to_s.tr(';', '') }.join(';') else raise StandardError, "TransArmor support is not enabled on your #{display_name} account" end end def money_from_authorization(auth) _, _, amount = auth.split(/;/, 3) amount.to_i end def message_from(response) if response[:faultcode] && response[:faultstring] response[:faultstring] elsif response[:error_number] && response[:error_number] != '0' response[:error_description] else result = (response[:exact_message] || '') result << " - #{response[:bank_message]}" if response[:bank_message].present? result end end def parse_error(error) { transaction_approved: 'false', error_number: error.code, error_description: error.body, ecommerce_error_code: error.body.gsub(/[^\d]/, '') } end def standard_error_code(response) STANDARD_ERROR_CODE_MAPPING[response[:bank_resp_code] || response[:ecommerce_error_code]] end def parse(xml) response = {} xml = REXML::Document.new(xml) if (root = REXML::XPath.first(xml, '//TransactionResult')) parse_elements(response, root) end SENSITIVE_FIELDS.each { |key| response.delete(key) } response end def parse_elements(response, root) root.elements.to_a.each do |node| if node.has_elements? parse_elements(response, node) else response[name_node(root, node)] = (node.text || '').strip end end end def name_node(root, node) parent = root.name unless root.name == 'TransactionResult' "#{parent}#{node.name}".gsub(/EXact/, 'Exact').underscore.to_sym end end end end