forked from activemerchant/active_merchant
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Douglas Pouk
committed
Sep 25, 2019
1 parent
2711a60
commit 0e46bc5
Showing
1 changed file
with
274 additions
and
0 deletions.
There are no files selected for viewing
274 changes: 274 additions & 0 deletions
274
lib/active_merchant/billing/gateways/quickbooks_oauth2.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
module ActiveMerchant #:nodoc: | ||
module Billing #:nodoc: | ||
class QuickbooksOauth2Gateway < Gateway | ||
self.test_url = 'https://sandbox.api.intuit.com' | ||
self.live_url = 'https://api.intuit.com' | ||
|
||
self.supported_countries = ['US'] | ||
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" | ||
|
||
# NOT USED ANYWHERE? | ||
# 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' | ||
# } | ||
|
||
# https://developer.intuit.com/docs/0150_payments/0300_developer_guides/error_handling | ||
|
||
STANDARD_ERROR_CODE_MAPPING = { | ||
# Fraud Warnings | ||
'PMT-1000' => STANDARD_ERROR_CODE[:processing_error], # payment was accepted, but refund was unsuccessful | ||
'PMT-1001' => STANDARD_ERROR_CODE[:invalid_cvc], # payment processed, but cvc was invalid | ||
'PMT-1002' => STANDARD_ERROR_CODE[:incorrect_address], # payment processed, incorrect address info | ||
'PMT-1003' => STANDARD_ERROR_CODE[:processing_error], # payment processed, address info couldn't be validated | ||
|
||
# Fraud Errors | ||
'PMT-2000' => STANDARD_ERROR_CODE[:incorrect_cvc], # Incorrect CVC | ||
'PMT-2001' => STANDARD_ERROR_CODE[:invalid_cvc], # CVC check unavaliable | ||
'PMT-2002' => STANDARD_ERROR_CODE[:incorrect_address], # Incorrect address | ||
'PMT-2003' => STANDARD_ERROR_CODE[:incorrect_address], # Address info unavailable | ||
|
||
'PMT-3000' => STANDARD_ERROR_CODE[:processing_error], # Merchant account could not be validated | ||
|
||
# Invalid Request | ||
'PMT-4000' => STANDARD_ERROR_CODE[:processing_error], # Object is invalid | ||
'PMT-4001' => STANDARD_ERROR_CODE[:processing_error], # Object not found | ||
'PMT-4002' => STANDARD_ERROR_CODE[:processing_error], # Object is required | ||
|
||
# Transaction Declined | ||
'PMT-5000' => STANDARD_ERROR_CODE[:card_declined], # Request was declined | ||
'PMT-5001' => STANDARD_ERROR_CODE[:card_declined], # Merchant does not support given payment method | ||
|
||
# System Error | ||
'PMT-6000' => STANDARD_ERROR_CODE[:processing_error], # A temporary Issue prevented this request from being processed. | ||
} | ||
|
||
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) | ||
@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) | ||
end | ||
|
||
def authorize(money, payment, options = {}) | ||
post = {} | ||
add_amount(post, money, options) | ||
add_charge_data(post, payment, options) | ||
post[:capture] = "false" | ||
|
||
commit(ENDPOINT, post) | ||
end | ||
|
||
def capture(money, authorization, options = {}) | ||
post = {} | ||
capture_uri = "#{ENDPOINT}/#{CGI.escape(authorization)}/capture" | ||
post[:amount] = localized_amount(money, currency(money)) | ||
add_context(post, options) | ||
|
||
commit(capture_uri, post) | ||
end | ||
|
||
def refund(money, authorization, options = {}) | ||
post = {} | ||
post[:amount] = localized_amount(money, currency(money)) | ||
add_context(post, options) | ||
|
||
commit(refund_uri(authorization), post) | ||
end | ||
|
||
def verify(credit_card, options = {}) | ||
authorize(1.00, credit_card, options) | ||
end | ||
|
||
def supports_scrubbing? | ||
true | ||
end | ||
|
||
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+)\d{16}), '\1[FILTERED]'). | ||
gsub(%r((cvc\D+)\d{3}), '\1[FILTERED]') | ||
end | ||
|
||
private | ||
|
||
def add_charge_data(post, payment, options = {}) | ||
add_payment(post, payment, options) | ||
add_address(post, options) | ||
end | ||
|
||
def add_address(post, options) | ||
return unless post[:card] && post[:card].kind_of?(Hash) | ||
|
||
card_address = {} | ||
if address = options[:billing_address] || options[:address] | ||
card_address[:streetAddress] = address[:address1] | ||
card_address[:city] = address[:city] | ||
card_address[:region] = address[:state] || address[:region] | ||
card_address[:country] = address[:country] | ||
card_address[:postalCode] = address[:zip] if address[:zip] | ||
end | ||
post[:card][:address] = card_address unless card_address.empty? | ||
end | ||
|
||
def add_amount(post, money, options = {}) | ||
currency = options[:currency] || currency(money) | ||
post[:amount] = localized_amount(money, currency) | ||
post[:currency] = currency.upcase | ||
end | ||
|
||
def add_payment(post, payment, options = {}) | ||
add_creditcard(post, payment, options) | ||
add_context(post, options) | ||
end | ||
|
||
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 | ||
|
||
def add_context(post, options = {}) | ||
post[:context] = { | ||
mobile: options.fetch(:mobile, false), | ||
isEcommerce: options.fetch(:ecommerce, true) | ||
} | ||
end | ||
|
||
def parse(body) | ||
JSON.parse(body) | ||
end | ||
|
||
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" | ||
end | ||
rescue ResponseError => e | ||
extract_response_body_or_raise(e) | ||
end | ||
response_object(response) | ||
end | ||
|
||
def response_object(raw_response) | ||
parsed_response = parse(raw_response) | ||
|
||
if parsed_response["code"] == "BadRequest" | ||
parsed_response["status"] = "BadRequest" | ||
parsed_response["message"] = "Check Quickbooks Connection" | ||
end | ||
Response.new( | ||
success?(parsed_response), | ||
message_from(parsed_response), | ||
parsed_response, | ||
authorization: authorization_from(parsed_response), | ||
test: test?, | ||
cvv_result: cvv_code_from(parsed_response), | ||
error_code: errors_from(parsed_response), | ||
fraud_review: fraud_review_status_from(parsed_response) | ||
) | ||
end | ||
|
||
def gateway_url | ||
test? ? test_url : live_url | ||
end | ||
|
||
def post_data(data = {}) | ||
data.to_json | ||
end | ||
|
||
def headers(method, uri) | ||
raise ArgumentError, "Invalid HTTP method: #{method}. Valid methods are :post and :get" unless [:post, :get].include?(method) | ||
|
||
{ | ||
"Content-type" => "application/json", | ||
"Request-Id" => generate_unique_id, | ||
#{}"Authorization" => oauth_headers.join(', ') | ||
"Accept" => "application/json", | ||
"Authorization" => "Bearer %s" % @options[:access_token], | ||
} | ||
end | ||
|
||
def cvv_code_from(response) | ||
if response['errors'].present? | ||
FRAUD_WARNING_CODES.include?(response['errors'].first['code']) ? 'I' : '' | ||
else | ||
success?(response) ? 'M' : '' | ||
end | ||
end | ||
|
||
def success?(response) | ||
return false if response["code"] == "BadRequest" | ||
return FRAUD_WARNING_CODES.concat(['0']).include?(response['errors'].first['code']) if response['errors'] | ||
|
||
!['DECLINED', 'CANCELLED'].include?(response['status']) | ||
end | ||
|
||
def message_from(response) | ||
response['errors'].present? ? response["errors"].map {|error_hash| error_hash["message"] }.join(" ") : response['status'] | ||
end | ||
|
||
def errors_from(response) | ||
response['errors'].present? ? STANDARD_ERROR_CODE_MAPPING[response["errors"].first["code"]] : "" | ||
end | ||
|
||
def authorization_from(response) | ||
response['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) | ||
begin | ||
parse(response_error.response.body) | ||
rescue JSON::ParserError | ||
raise response_error | ||
end | ||
response_error.response.body | ||
end | ||
|
||
def refund_uri(authorization) | ||
"#{ENDPOINT}/#{CGI.escape(authorization)}/refunds" | ||
end | ||
end | ||
end | ||
end |