lib/active_merchant/billing/gateways/quickbooks.rb in activemerchant-1.100.0 vs lib/active_merchant/billing/gateways/quickbooks.rb in activemerchant-1.101.0
- old
+ new
@@ -8,17 +8,14 @@
self.default_currency = 'USD'
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners]
self.homepage_url = 'http://payments.intuit.com'
self.display_name = 'QuickBooks Payments'
- ENDPOINT = '/quickbooks/v4/payments/charges'
- OAUTH_ENDPOINTS = {
- site: 'https://oauth.intuit.com',
- request_token_path: '/oauth/v1/get_request_token',
- authorize_url: 'https://appcenter.intuit.com/Connect/Begin',
- access_token_path: '/oauth/v1/get_access_token'
- }
+ BASE = '/quickbooks/v4/payments'
+ ENDPOINT = "#{BASE}/charges"
+ VOID_ENDPOINT = "#{BASE}/txn-requests"
+ REFRESH_URI = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer'
# https://developer.intuit.com/docs/0150_payments/0300_developer_guides/error_handling
STANDARD_ERROR_CODE_MAPPING = {
# Fraud Warnings
@@ -49,50 +46,70 @@
}
FRAUD_WARNING_CODES = ['PMT-1000', 'PMT-1001', 'PMT-1002', 'PMT-1003']
def initialize(options = {})
- requires!(options, :consumer_key, :consumer_secret, :access_token, :token_secret, :realm)
+ # 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
def purchase(money, payment, options = {})
post = {}
add_amount(post, money, options)
add_charge_data(post, payment, options)
post[:capture] = 'true'
- commit(ENDPOINT, post)
+ response = commit(ENDPOINT, post)
+ check_token_response(response, ENDPOINT, post)
end
def authorize(money, payment, options = {})
post = {}
add_amount(post, money, options)
add_charge_data(post, payment, options)
post[:capture] = 'false'
- commit(ENDPOINT, post)
+ response = commit(ENDPOINT, post)
+ check_token_response(response, ENDPOINT, post)
end
def capture(money, authorization, options = {})
post = {}
- capture_uri = "#{ENDPOINT}/#{CGI.escape(authorization)}/capture"
+ authorization, _ = split_authorization(authorization)
post[:amount] = localized_amount(money, currency(money))
add_context(post, options)
- commit(capture_uri, post)
+ response = commit(capture_uri(authorization), post)
+ check_token_response(response, capture_uri(authorization), post)
end
def refund(money, authorization, options = {})
post = {}
post[:amount] = localized_amount(money, currency(money))
add_context(post, options)
+ authorization, _ = split_authorization(authorization)
- commit(refund_uri(authorization), post)
+ response = commit(refund_uri(authorization), post)
+ check_token_response(response, refund_uri(authorization), post)
end
+ def void(authorization, options = {})
+ _, request_id = split_authorization(authorization)
+
+ response = commit(void_uri(request_id))
+ check_token_response(response, void_uri(request_id))
+ end
+
def verify(credit_card, options = {})
authorize(1.00, credit_card, options)
end
def supports_scrubbing?
@@ -105,11 +122,16 @@
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+)\d{16}), '\1[FILTERED]').
- gsub(%r((cvc\D+)\d{3}), '\1[FILTERED]')
+ gsub(%r((cvc\D+)\d{3}), '\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
private
def add_charge_data(post, payment, options = {})
@@ -168,34 +190,34 @@
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.
- response = begin
- case method
- when :post
- ssl_post(endpoint, post_data(body), headers(:post, endpoint))
- when :get
- ssl_request(:get, endpoint, nil, headers(:get, endpoint))
- else
- raise ArgumentError, "Invalid HTTP method: #{method}. Valid methods are :post and :get"
+ 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
- rescue ResponseError => e
- extract_response_body_or_raise(e)
- end
- response_object(response)
+ response_object(response, headers)
end
- def response_object(raw_response)
+ 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),
+ 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)
)
@@ -208,10 +230,12 @@
def post_data(data = {})
data.to_json
end
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 [:post, :get].include?(method)
request_uri = URI.parse(uri)
# Following the guidelines from http://nouncer.com/oauth/authentication.html
oauth_parameters = {
@@ -241,10 +265,46 @@
'Request-Id' => generate_unique_id,
'Authorization' => oauth_headers.join(', ')
}
end
+ def oauth_v2_headers
+ {
+ 'Content-Type' => 'application/json',
+ 'Request-Id' => generate_unique_id,
+ 'Accept' => 'application/json',
+ 'Authorization' => "Bearer #{@options[:access_token]}"
+ }
+ end
+
+ def check_token_response(response, endpoint, body = {})
+ return response unless @options[:refresh_token]
+ return response unless response.params['code'] == 'AuthenticationFailed'
+ refresh_access_token
+ commit(endpoint, body)
+ end
+
+ 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)
+ response = JSON.parse(response)
+
+ @options[:access_token] = response['access_token'] if response['access_token']
+ @options[:refresh_token] = response['refresh_token'] if response['refresh_token']
+ end
+
def cvv_code_from(response)
if response['errors'].present?
FRAUD_WARNING_CODES.include?(response['errors'].first['code']) ? 'I' : ''
else
success?(response) ? 'M' : ''
@@ -263,14 +323,19 @@
def errors_from(response)
response['errors'].present? ? STANDARD_ERROR_CODE_MAPPING[response['errors'].first['code']] : ''
end
- def authorization_from(response)
- response['id']
+ def authorization_from(response, headers = {})
+ [response['id'], headers['Request-Id']].join('|')
end
+ def split_authorization(authorization)
+ authorization, request_id = authorization.split('|')
+ [authorization, request_id]
+ end
+
def fraud_review_status_from(response)
response['errors'] && FRAUD_WARNING_CODES.include?(response['errors'].first['code'])
end
def extract_response_body_or_raise(response_error)
@@ -281,10 +346,18 @@
end
response_error.response.body
end
def refund_uri(authorization)
- "#{ENDPOINT}/#{CGI.escape(authorization)}/refunds"
+ "#{ENDPOINT}/#{CGI.escape(authorization.to_s)}/refunds"
+ end
+
+ def capture_uri(authorization)
+ "#{ENDPOINT}/#{CGI.escape(authorization.to_s)}/capture"
+ end
+
+ def void_uri(request_id)
+ "#{VOID_ENDPOINT}/#{CGI.escape(request_id.to_s)}/void"
end
end
end
end