class ActiveMerchant::Billing::QuickbooksGateway

Constants

BASE
ENDPOINT
FRAUD_WARNING_CODES
REFRESH_URI
STANDARD_ERROR_CODE_MAPPING

developer.intuit.com/docs/0150_payments/0300_developer_guides/error_handling

VOID_ENDPOINT

Public Class Methods

new(options = {}) click to toggle source
Calls superclass method ActiveMerchant::Billing::Gateway::new
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 50
def initialize(options = {})
  # Quickbooks is deprecating OAuth 1.0 on December 17, 2019.
  # OAuth 2.0 requires a client_id, client_secret, access_token, and refresh_token
  # To maintain backwards compatibility, check for the presence of a refresh_token (only specified for OAuth 2.0)
  # When present, validate that all OAuth 2.0 options are present
  if options[:refresh_token]
    requires!(options, :client_id, :client_secret, :access_token, :refresh_token)
  else
    requires!(options, :consumer_key, :consumer_secret, :access_token, :token_secret, :realm)
  end
  @options = options
  super
end

Public Instance Methods

authorize(money, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 74
def authorize(money, payment, options = {})
  post = {}
  add_amount(post, money, options)
  add_charge_data(post, payment, options)
  post[:capture] = 'false'

  response = commit(ENDPOINT, post)
  check_token_response(response, ENDPOINT, post, options)
end
capture(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 84
def capture(money, authorization, options = {})
  post = {}
  authorization, = split_authorization(authorization)
  post[:amount] = localized_amount(money, currency(money))
  add_context(post, options)

  response = commit(capture_uri(authorization), post)
  check_token_response(response, capture_uri(authorization), post, options)
end
purchase(money, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 64
def purchase(money, payment, options = {})
  post = {}
  add_amount(post, money, options)
  add_charge_data(post, payment, options)
  post[:capture] = 'true'

  response = commit(ENDPOINT, post)
  check_token_response(response, ENDPOINT, post, options)
end
refresh() click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 115
def refresh
  response = refresh_access_token
  response_object(response)
end
refund(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 94
def refund(money, authorization, options = {})
  post = {}
  post[:amount] = localized_amount(money, currency(money))
  add_context(post, options)
  authorization, = split_authorization(authorization)

  response = commit(refund_uri(authorization), post)
  check_token_response(response, refund_uri(authorization), post, options)
end
scrub(transcript) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 124
def scrub(transcript)
  transcript.
    gsub(%r((realm=\")\w+), '\1[FILTERED]').
    gsub(%r((oauth_consumer_key=\")\w+), '\1[FILTERED]').
    gsub(%r((oauth_nonce=\")\w+), '\1[FILTERED]').
    gsub(%r((oauth_signature=\")[a-zA-Z%0-9]+), '\1[FILTERED]').
    gsub(%r((oauth_token=\")\w+), '\1[FILTERED]').
    gsub(%r((number\\\":\\\")\d+), '\1[FILTERED]').
    gsub(%r((cvc\\\":\\\")\d+), '\1[FILTERED]').
    gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
    gsub(%r((access_token\\?":\\?")[\w\-\.]+)i, '\1[FILTERED]').
    gsub(%r((refresh_token\\?":\\?")\w+), '\1[FILTERED]').
    gsub(%r((refresh_token=)\w+), '\1[FILTERED]').
    gsub(%r((Authorization: Bearer )[\w\-\.]+)i, '\1[FILTERED]\2')
end
supports_scrubbing?() click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 120
def supports_scrubbing?
  true
end
verify(credit_card, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 111
def verify(credit_card, options = {})
  authorize(1.00, credit_card, options)
end
void(authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 104
def void(authorization, options = {})
  _, request_id = split_authorization(authorization)

  response = commit(void_uri(request_id))
  check_token_response(response, void_uri(request_id), {}, options)
end

Private Instance Methods

add_address(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 147
def add_address(post, options)
  return unless post[:card]&.kind_of?(Hash)

  card_address = {}
  if address = options[:billing_address] || options[:address]
    card_address[:streetAddress] = address[:address1]
    card_address[:city] = address[:city]
    region = address[:state] || address[:region]
    card_address[:region] = region if region.present?
    card_address[:country] = address[:country] if address[:country].present?
    card_address[:postalCode] = address[:zip] if address[:zip]
  end
  post[:card][:address] = card_address
end
add_amount(post, money, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 162
def add_amount(post, money, options = {})
  currency = options[:currency] || currency(money)
  post[:amount] = localized_amount(money, currency)
  post[:currency] = currency.upcase
end
add_charge_data(post, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 142
def add_charge_data(post, payment, options = {})
  add_payment(post, payment, options)
  add_address(post, options)
end
add_context(post, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 185
def add_context(post, options = {})
  post[:context] = {
    mobile: options.fetch(:mobile, false),
    isEcommerce: options.fetch(:ecommerce, true)
  }
end
add_creditcard(post, creditcard, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 173
def add_creditcard(post, creditcard, options = {})
  card = {}
  card[:number] = creditcard.number
  card[:expMonth] = '%02d' % creditcard.month
  card[:expYear] = creditcard.year
  card[:cvc] = creditcard.verification_value if creditcard.verification_value?
  card[:name] = creditcard.name if creditcard.name
  card[:commercialCardCode] = options[:card_code] if options[:card_code]

  post[:card] = card
end
add_payment(post, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 168
def add_payment(post, payment, options = {})
  add_creditcard(post, payment, options)
  add_context(post, options)
end
authorization_from(response, headers = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 342
def authorization_from(response, headers = {})
  [response['id'], headers['Request-Id']].join('|')
end
capture_uri(authorization) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 369
def capture_uri(authorization)
  "#{ENDPOINT}/#{CGI.escape(authorization.to_s)}/capture"
end
check_token_response(response, endpoint, body = {}, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 286
def check_token_response(response, endpoint, body = {}, options = {})
  return response unless @options[:refresh_token]
  return response unless options[:allow_refresh]
  return response unless response.params['code'] == 'AuthenticationFailed'

  refresh_access_token
  commit(endpoint, body)
end
commit(uri, body = {}, method = :post) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 196
def commit(uri, body = {}, method = :post)
  endpoint = gateway_url + uri
  # The QuickBooks API returns HTTP 4xx on failed transactions, which causes a
  # ResponseError raise, so we have to inspect the response and discern between
  # a legitimate HTTP error and an actual gateway transactional error.
  headers = {}
  response =
    begin
      headers = headers(method, endpoint)
      method == :post ? ssl_post(endpoint, post_data(body), headers) : ssl_request(:get, endpoint, nil, headers)
    rescue ResponseError => e
      extract_response_body_or_raise(e)
    end

  response_object(response, headers)
end
cvv_code_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 316
def cvv_code_from(response)
  if response['errors'].present?
    FRAUD_WARNING_CODES.include?(response['errors'].first['code']) ? 'I' : ''
  else
    success?(response) ? 'M' : ''
  end
end
errors_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 334
def errors_from(response)
  if %w[AuthenticationFailed AuthorizationFailed].include?(response['code'])
    response['code']
  else
    response['errors'].present? ? STANDARD_ERROR_CODE_MAPPING[response['errors'].first['code']] : ''
  end
end
extract_response_body_or_raise(response_error) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 355
def extract_response_body_or_raise(response_error)
  begin
    parse(response_error.response.body)
  rescue JSON::ParserError
    raise response_error
  end

  response_error.response.body
end
fraud_review_status_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 351
def fraud_review_status_from(response)
  response['errors'] && FRAUD_WARNING_CODES.include?(response['errors'].first['code'])
end
gateway_url() click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 232
def gateway_url
  test? ? test_url : live_url
end
headers(method, uri) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 240
def headers(method, uri)
  return oauth_v2_headers if @options[:refresh_token]

  raise ArgumentError, "Invalid HTTP method: #{method}. Valid methods are :post and :get" unless %i[post get].include?(method)

  request_uri = URI.parse(uri)

  # Following the guidelines from http://nouncer.com/oauth/authentication.html
  oauth_parameters = {
    oauth_nonce: generate_unique_id,
    oauth_timestamp: Time.now.to_i.to_s,
    oauth_signature_method: 'HMAC-SHA1',
    oauth_version: '1.0',
    oauth_consumer_key: @options[:consumer_key],
    oauth_token: @options[:access_token]
  }

  # prepare components for signature
  oauth_signature_base_string = [method.to_s.upcase, request_uri.to_s, oauth_parameters.to_param].map { |v| CGI.escape(v) }.join('&')
  oauth_signing_key = [@options[:consumer_secret], @options[:token_secret]].map { |v| CGI.escape(v) }.join('&')
  hmac_signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), oauth_signing_key, oauth_signature_base_string)

  # append signature to required OAuth parameters
  oauth_parameters[:oauth_signature] = CGI.escape(Base64.encode64(hmac_signature).chomp.delete("\n"))

  # prepare Authorization header string
  oauth_parameters = oauth_parameters.sort_by { |k, _| k }.to_h
  oauth_headers = ["OAuth realm=\"#{@options[:realm]}\""]
  oauth_headers += oauth_parameters.map { |k, v| "#{k}=\"#{v}\"" }

  {
    'Content-type' => 'application/json',
    'Request-Id' => generate_unique_id,
    'Authorization' => oauth_headers.join(', ')
  }
end
message_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 330
def message_from(response)
  response['errors'].present? ? response['errors'].map { |error_hash| error_hash['message'] }.join(' ') : response['status']
end
oauth_v2_headers() click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 277
def oauth_v2_headers
  {
    'Content-Type'      => 'application/json',
    'Request-Id'        => generate_unique_id,
    'Accept'            => 'application/json',
    'Authorization'     => "Bearer #{@options[:access_token]}"
  }
end
parse(body) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 192
def parse(body)
  JSON.parse(body)
end
post_data(data = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 236
def post_data(data = {})
  data.to_json
end
refresh_access_token() click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 295
def refresh_access_token
  post = {}
  post[:grant_type] = 'refresh_token'
  post[:refresh_token] = @options[:refresh_token]
  data = post.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')

  basic_auth = Base64.strict_encode64("#{@options[:client_id]}:#{@options[:client_secret]}")
  headers = {
    'Content-Type'      => 'application/x-www-form-urlencoded',
    'Accept'            => 'application/json',
    'Authorization'     => "Basic #{basic_auth}"
  }

  response = ssl_post(REFRESH_URI, data, headers)
  json_response = JSON.parse(response)

  @options[:access_token] = json_response['access_token'] if json_response['access_token']
  @options[:refresh_token] = json_response['refresh_token'] if json_response['refresh_token']
  response
end
refund_uri(authorization) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 365
def refund_uri(authorization)
  "#{ENDPOINT}/#{CGI.escape(authorization.to_s)}/refunds"
end
response_object(raw_response, headers = {}) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 213
def response_object(raw_response, headers = {})
  parsed_response = parse(raw_response)

  # Include access_token and refresh_token in params for OAuth 2.0
  parsed_response['access_token'] = @options[:access_token] if @options[:refresh_token]
  parsed_response['refresh_token'] = @options[:refresh_token] if @options[:refresh_token]

  Response.new(
    success?(parsed_response),
    message_from(parsed_response),
    parsed_response,
    authorization: authorization_from(parsed_response, headers),
    test: test?,
    cvv_result: cvv_code_from(parsed_response),
    error_code: errors_from(parsed_response),
    fraud_review: fraud_review_status_from(parsed_response)
  )
end
split_authorization(authorization) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 346
def split_authorization(authorization)
  authorization, request_id = authorization.split('|')
  [authorization, request_id]
end
success?(response) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 324
def success?(response)
  return FRAUD_WARNING_CODES.concat(['0']).include?(response['errors'].first['code']) if response['errors']

  !%w[DECLINED CANCELLED].include?(response['status']) && !%w[AuthenticationFailed AuthorizationFailed].include?(response['code'])
end
void_uri(request_id) click to toggle source
# File lib/active_merchant/billing/gateways/quickbooks.rb, line 373
def void_uri(request_id)
  "#{VOID_ENDPOINT}/#{CGI.escape(request_id.to_s)}/void"
end