Migrating a QuickBooks Online App from OAuth1 to OAuth2 using the qbo_api gem - Step 1
OAuth2 Renewal Strategy
The QuickBooks Online API OAuth2 access token's are only good for 1 hour. This is a radical departure from OAuth1's 6 month expiry duration. With that short a window probably the best way to ensure uninterrupted usage is to utilize a code pattern for all QBO request that does the following:
- Catches "Unauthorized" errors (status code 401 — i.e. an invalid access token).
- Next, automatically renews and persists the access token.
- Lastly, replays the failed 401 request.
In the screencast, I'll be focusing on this approach. See the show notes below for an example code pattern that will make OAuth2 renewal and persistence automatic. Note: The code pattern below requires customization for your particular situation.
Scheduled Renewal Job?
With OAuth1's one month renewal window you could rely on a background scheduler like Cron but with the OAuth2's super short renewal duration, relying solely on Cron may not be best because in the case that your app or the QBO API has an outage when either service returns it may take considerably longer to get QBO requests flowing again. I talk more about going with a background scheduler strategy here.
Show Notes:
- Update the
qbo_apigem to the latest. - Refactor your QuickBooks Online API requests in to the "request_handler" pattern as seen below. The qbo_account, record, and payload ideas are specific to my app that I'm switching over to OAuth2. Please watch the screencast to get the proper context.
module Qbo
class Base
def self.request_handler(qbo_account_id:, payload: false, record: false)
retries ||= 0
qbo_account = QboAccount.find(qbo_account_id)
api = Qbo.init(qbo_account)
yield(api)
rescue QboApi::Unauthorized
if qbo_account.access_token.present?
Qbo::OAuth2.renew(qbo_account)
retry if (retries += 1) < 3
else
register_error(qbo_account: qbo_account, error: '401 Authentication Error', payload: payload)
end
rescue QboApi::Error => e
if err = e.fault
if record
fix_duplicate_customer(qbo_account: qbo_account, record: record, error: err, payload: payload, qbo_api: api)
else
final_err = error_body(err)
register_error(qbo_account: qbo_account, error: final_err, payload: payload)
end
end
rescue => e
final_err = e.to_s[0..255]
qbo_account.account.qbo_errors.create(body: final_err, request_json: payload.to_json, account: qbo_account.account)
raise e
end
def self.entity_handler(qbo_account_id:, entity:, record:, payload:)
request_handler(qbo_account_id: qbo_account_id, record: record, payload: payload) do |api|
if id = record.qbo_id
res = api.update(entity, id: id, payload: payload)
else
res = api.create(entity, payload: payload)
record.update(qbo_id: res['Id'])
end
res
end
end
def self.fix_duplicate_customer(qbo_account:, record:, error:, payload:, qbo_api: api)
if record.class.name == "Customer" && error[:error_body][0][:error_message] == 'Duplicate Name Exists Error'
if res = qbo_api.get(:customer, ["DisplayName", record.display_name])
record.update(qbo_id: res['Id'])
else
register_error(qbo_account: qbo_account, error: error, payload: payload, resource: record)
end
else
register_error(qbo_account: qbo_account, error: error, payload: payload, resource: record)
end
end
def self.register_error(qbo_account:, error:, payload:, resource: false)
qbo_account.account.qbo_errors.create(body: error[:error_body], method: error[:method], resource: resource,
url: error[:url], status: error[:status], request_json: payload.to_json)
end
def self.error_body(err)
if body = err.try(:[], :error_body)
result = body.map{ |b| b.try(:[], :error_detail) || b.try(:[], :error_message) }.join(" -- ")
else
""
end
end
end
end
module Qbo
def self.init(qbo_account)
if qbo_account.access_token.present?
attrs = {
access_token: qbo_account.access_token,
realm_id: qbo_account.companyid
}
else
attrs = {
token: qbo_account.token,
token_secret: qbo_account.secret,
realm_id: qbo_account.companyid,
consumer_key: Rails.application.secrets.qbo_app_consumer_key,
consumer_secret: Rails.application.secrets.qbo_app_consumer_secret
}
end
QboApi.new(attrs)
end
end
- Pushed on 10/24/2019 by Christian
- QuickBooks Integration Consulting
