Rails engine that is all about handling and responding to QBO API errors.
When integrating with QuickBooks Online (QBO) there is one thing you better be prepared for: errors and lots of 'em.
The qbo_rails
gem is a Rails engine that is all about handling and responding to QBO errors. It does some other common things as well such as automatically determining and submitting create or update requests.
The qbo_rails
gem depends on both the quickbooks-ruby and the quickbooks-ruby-base. In essence, it is a thin wrapper around the CRUD actions of quickbooks-ruby
. The lib/qbo_rails.rb
source is only ~100 lines of code.
gem 'qbo_rails'
bundle
rails generate qbo_rails:install
config/initializers/qbo_rails.rb
qbo_errors
tableapp/models/qbo_error.rb
bin/rake db:migrate
config/secrets.yml
development: &default
...
qbo_app_consumer_key: <%= ENV['YOUR_QBO_APP_DEV_CONSUMER_KEY'] %>
qbo_app_consumer_secret: <%= ENV['YOUR_QBO_APP_DEV_CONSUMER_SECRET'] %>
...
config/initializers/qbo_rails.rb
The gem assumes that you store QBO ids in a column/attribute called qbo_id
. You can change that as so:
QboRails.foreign_key = 'qbo_external_id'
QboRails
.base()
The quickbooks-ruby-base
gem is composed into a new instance of QboRails
and can accessed as so:
qbo_rails = QboRails.new(account, :customer)
qbo_rails.base
You can access the quickbooks-ruby
instance through .base
. See the docs.
.create_or_update()
You must keep entities in sync between your app and QBO. Using the create_or_update
method qbo_rails
will send a create
or update
based on if say customer.qbo_id
is nil.
Example:
account = Account.find(1) # this is the where the QBO OAuth access_token, secret, and company_id are found
customer = Customer.find(1)
qbo_rails = QboRails.new(account, :customer)
qb_customer = qbo_rails.base.qr_model(:customer)
qb_customer.display_name = customer.display_name
address = qbo_rails.base.qr_model(:physical_address)
address.line1 = customer.address_1
address.city = customer.city
address.country_sub_division_code = customer.state
address.postal_code = customer.zip
qb_customer.billing_address = address
qbo_rails.create_or_update(customer, qb_customer)
If customer.qbo_id
is nil
then a create request is sent to QBO. If successful the QBO Id for the new Customer record will be automatically recorded in customer.qbo_id
. Therefore, the next time qbo_rails.create_or_update(customer, qbo_customer)
for this customer is called an update
request will be sent. Sending an update first involves querying QBO to get the latest sync token so it is nice to DRY up that procedure.
.create()
If you don’t need to do an update and don’t need to record the QBO Id in a model but still want the error handling goodness then the create()
method is available
Example:
qbo_rails = QboRails.new(account, :customer)
qb_customer = qbo_rails.base.qr_model(:customer)
qb_customer.display_name = 'Hockey Mom'
qbo_rails.create(qbo_customer)
.delete()
The delete
method takes either the ActiveRecord instance or a QBO Id.
Example: This deletes the QBO Customer with Id = 1
qbo_rails = QboRails.new(account, :customer)
qbo_rails.delete(1)
Example: Pass in ActiveRecord instance
customer = account.customers.find(2)
qbo_rails = QboRails.new(account, :customer)
qbo_rails.delete(customer)
Use qbo_rails.result
. e.g.
qbo_rails.create_or_update(customer, qbo_customer)
puts qbo_rails.result.id
QBO API Errors are recorded in the QboError
model. The column names are:
=> QboError(id: integer,
message: string,
body: text,
resource_type: string,
resource_id: integer,
request_xml: text,
created_at: datetime,
updated_at: datetime
)
For example, let’s say I want to record an association to a qbo_account
model. Use the before_create
callback to populate the column. e.g.:
class QboError < ActiveRecord::Base
belongs_to :resource, polymorphic: true
belongs_to :qbo_account
before_create :set_qbo_account
private
def set_qbo_account
self.qbo_account = self.resource.try(:import_file).try(:qbo_account)
end
end
The columns resource_type
and resource_id
are for recording the ActiveRecord model and are ready-to-go for polymorphic associations.
Let’s say that you have a customer in Rails, Jane Riley, and you send her in as a create request to QBO. There is already a Jane Riley on QBO. Guess what? Duplicate Name Exists Error
. But qbo_rails
can handle this by simple adding the below method with the prefix handle_error
.
For example in your Rails app you could add a file in app/services/qbo_rails/error_handler.rb
that responds to QBO API’s Duplicate Name Exists Error
.
class QboRails
module ErrorHandler
def handle_error_name_entity_already_exists(exception)
if exception.message =~ /Duplicate Name Exists Error.*Another (customer|vendor|employee)/m
display_name = Nokogiri::XML(exception.request_xml).at('DisplayName').content
result = @base.find_by_display_name(display_name)
if result.entries.size == 1
@record.update_column(foreign_key, result.entries.first.id)
@only_run_once = true
@record.reload
create_or_update(@record, @qb_record)
true
else
false
end
else
false
end
end
end
It catches the error, queries QBO for Jane Riley, sets the customer record with the Id, and then calls create_or_update
, which will properly send an update request. You have access to all QboRails
instance variables such as:
@record: This is the ActiveRecord instance (only available if passed)
@qb_record: This is the `quickbooks-ruby` instance.
@base: This is the `quickbooks-ruby-base` instance.
See the source to get all the instance vars and methods available to you when crafting custom error handling.
true
after you have run a response action.@only_run_once = true
when returning true
.false
.These rules are for preventing infinite looping. See the spec/dummy
code for more examples.
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)Note: If you are going to adding new specs or modify existing ones that involve a transaction with the QBO API then they must be recorded using VCR. To do that set your test QBO app and sandbox credentials in spec/dummy/.env
file (don’t commit this in your PR). The .env
file should be in this format:
export QBO_RAILS_CONSUMER_KEY=
export QBO_RAILS_CONSUMER_SECRET=
export QBO_RAILS_ACCESS_TOKEN=
export QBO_RAILS_ACCESS_TOKEN_SECRET=
export QBO_RAILS_COMPANY_ID=
To make it easy to get your OAuth information you can:
cd spec/dummy
bin/rails s
localhost:3000
.env
variables for QBO_RAILS_ACCESS_TOKEN
, QBO_RAILS_ACCESS_TOKEN_SECRET
, and QBO_RAILS_COMPANY_ID
will be displayed.