module ActiveMerchant #:nodoc: module Billing #:nodoc: class PaysafeGateway < Gateway self.test_url = 'https://api.test.paysafe.com' self.live_url = 'https://api.paysafe.com' self.supported_countries = %w(FR) self.default_currency = 'EUR' self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.paysafe.com/' self.display_name = 'Paysafe' def initialize(options = {}) requires!(options, :username, :password, :account_id) super end def purchase(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) add_billing_address(post, options) add_merchant_details(post, options) add_customer_data(post, payment, options) unless payment.is_a?(String) add_three_d_secure(post, payment, options) if options[:three_d_secure] post[:settleWithAuth] = true commit(:post, 'auths', post, options) end def authorize(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) add_billing_address(post, options) add_merchant_details(post, options) add_customer_data(post, payment, options) unless payment.is_a?(String) add_three_d_secure(post, payment, options) if options[:three_d_secure] commit(:post, 'auths', post, options) end def capture(money, authorization, options = {}) post = {} add_invoice(post, money, options) commit(:post, "auths/#{authorization}/settlements", post, options) end def refund(money, authorization, options = {}) post = {} add_invoice(post, money, options) commit(:post, "settlements/#{authorization}/refunds", post, options) end def void(authorization, options = {}) post = {} money = options[:amount] add_invoice(post, money, options) commit(:post, "auths/#{authorization}/voidauths", post, options) end def credit(money, payment, options = {}) post = {} add_invoice(post, money, options) add_payment(post, payment) commit(:post, 'standalonecredits', post, options) end # This is a '$0 auth' done at a specific verification endpoint at the gateway def verify(payment, options = {}) post = {} add_payment(post, payment) add_billing_address(post, options) add_customer_data(post, payment, options) unless payment.is_a?(String) commit(:post, 'verifications', post, options) end def store(payment, options = {}) post = {} add_payment(post, payment) add_address_for_vaulting(post, options) add_profile_data(post, payment, options) add_store_data(post, payment, options) commit(:post, 'profiles', post, options) end def redact(pm_profile_id) commit_for_redact(:delete, "profiles/#{pm_profile_id}", nil, nil) end def supports_scrubbing? true end def scrub(transcript) transcript. gsub(%r((Authorization: Basic )[a-zA-Z0-9:_]+), '\1[FILTERED]'). gsub(%r(("cardNum\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r(("cvv\\?":\\?")\d+), '\1[FILTERED]') end private # Customer data can be included in transactions where the payment method is a credit card # but should not be sent when the payment method is a token def add_customer_data(post, creditcard, options) post[:profile] = {} post[:profile][:firstName] = creditcard.first_name post[:profile][:lastName] = creditcard.last_name post[:profile][:email] = options[:email] if options[:email] post[:customerIp] = options[:ip] if options[:ip] end def add_billing_address(post, options) return unless options[:billing_address] || options[:address] address = options[:billing_address] || options[:address] post[:billingDetails] = {} post[:billingDetails][:street] = address[:address1] post[:billingDetails][:city] = address[:city] post[:billingDetails][:state] = address[:state] post[:billingDetails][:country] = address[:country] post[:billingDetails][:zip] = address[:zip] post[:billingDetails][:phone] = address[:phone] end # The add_address_for_vaulting method is applicable to the store method, as the APIs address # object is formatted differently from the standard transaction billing address def add_address_for_vaulting(post, options) return unless options[:billing_address || options[:address]] address = options[:billing_address] || options[:address] post[:billingAddress] = {} post[:billingAddress][:street] = address[:address1] post[:billingAddress][:city] = address[:city] post[:billingAddress][:zip] = address[:zip] post[:billingAddress][:country] = address[:country] post[:billingAddress][:state] = address[:state] if address[:state] end # This data is specific to creating a profile at the gateway's vault level def add_profile_data(post, payment, options) address = options[:billing_address] || options[:address] post[:firstName] = payment.first_name post[:lastName] = payment.last_name post[:dateOfBirth] = {} post[:dateOfBirth][:year] = options[:date_of_birth][:year] post[:dateOfBirth][:month] = options[:date_of_birth][:month] post[:dateOfBirth][:day] = options[:date_of_birth][:day] post[:email] = options[:email] if options[:email] post[:phone] = (address[:phone] || options[:phone]) if address[:phone] || options[:phone] post[:ip] = options[:ip] if options[:ip] end def add_store_data(post, payment, options) post[:merchantCustomerId] = options[:customer_id] || SecureRandom.hex(12) post[:locale] = options[:locale] || 'en_US' post[:card][:holderName] = payment.name end # Paysafe expects minor units so we are not calling amount method on money parameter def add_invoice(post, money, options) post[:amount] = money end def add_payment(post, payment) if payment.is_a?(String) post[:card] = {} post[:card][:paymentToken] = payment else post[:card] = { cardExpiry: {} } post[:card][:cardNum] = payment.number post[:card][:cardExpiry][:month] = payment.month post[:card][:cardExpiry][:year] = payment.year post[:card][:cvv] = payment.verification_value end end def add_merchant_details(post, options) return unless options[:merchant_descriptor] post[:merchantDescriptor] = {} post[:merchantDescriptor][:dynamicDescriptor] = options[:merchant_descriptor][:dynamic_descriptor] if options[:merchant_descriptor][:dynamic_descriptor] post[:merchantDescriptor][:phone] = options[:merchant_descriptor][:phone] if options[:merchant_descriptor][:phone] end def add_three_d_secure(post, payment, options) three_d_secure = options[:three_d_secure] post[:authentication] = {} post[:authentication][:eci] = three_d_secure[:eci] post[:authentication][:cavv] = three_d_secure[:cavv] post[:authentication][:xid] = three_d_secure[:xid] if three_d_secure[:xid] post[:authentication][:threeDSecureVersion] = three_d_secure[:version] post[:authentication][:directoryServerTransactionId] = three_d_secure[:ds_transaction_id] unless payment.is_a?(String) || payment.brand != 'mastercard' end def parse(body) JSON.parse(body) end def commit(method, action, parameters, options) url = url(action) raw_response = ssl_request(method, url, post_data(parameters, options), headers) response = parse(raw_response) success = success_from(response) Response.new( success, message_from(success, response), response, authorization: authorization_from(response), avs_result: AVSResult.new(code: response['avsResponse']), cvv_result: CVVResult.new(response['cvvVerification']), test: test?, error_code: success ? nil : error_code_from(response) ) end def commit_for_redact(method, action, parameters, options) url = url(action) response = raw_ssl_request(method, url, post_data(parameters, options), headers) success = true if response.code == '200' Response.new( success, message: response.message ) end def headers { 'Content-Type' => 'application/json', 'Authorization' => "Basic #{Base64.strict_encode64(@options[:api_key].to_s)}" } end def url(action, options = {}) base_url = (test? ? test_url : live_url) if action.include? 'profiles' "#{base_url}/customervault/v1/#{action}" else "#{base_url}/cardpayments/v1/accounts/#{@options[:account_id]}/#{action}" end end def success_from(response) return false if response['status'] == 'FAILED' || response['error'] true end def message_from(success, response) return response['status'] unless response['error'] "Error(s)- code:#{response['error']['code']}, message:#{response['error']['message']}" end def authorization_from(response) response['id'] end def post_data(parameters = {}, options = {}) return unless parameters.present? parameters[:merchantRefNum] = options[:merchant_ref_num] || SecureRandom.hex(16).to_s parameters.to_json end def error_code_from(response) return unless response['error'] response['error']['code'] end def handle_response(response) response.body end end end end