minimul / qbo_api

Ruby JSON-only client for QuickBooks Online API v3. Built on top of the Faraday gem.
MIT License
85 stars 46 forks source link

QboApi

Ruby client for the QuickBooks Online API version 3.

The Book

The QBO book

Ruby >= 2.6 required

Installation

Add this line to your application's Gemfile:

gem 'qbo_api'

And then execute:

$ bundle

Or install it yourself as:

$ gem install qbo_api

Usage

Initialize

  qbo_api = QboApi.new(access_token: 'REWR342532asdfae!$4asdfa', realm_id: 32095430444)
- qbo_api.get :customer, 1

Super fast way to use QboApi as long as Ruby >= 2.5 is installed

- cd ~/<local dir>
- git clone git@github.com:minimul/qbo_api.git && cd qbo_api
- bundle
- bin/console
- QboApi.production = true
- qbo_api = QboApi.new(access_token: "qyprd2uvCOdRq8xzoSSiiiiii", realm_id: "12314xxxxxx7")
- qbo_api.get :customer, 1

DateTime serialization

Some QBO entities have attributes of type DateTime (e.g., Time Activities with StartTime and EndTime). All DateTimes passed to the QBO API must be serialized in ISO 8601 format. If ActiveSupport is loaded, you can achieve proper serialization with the following configuration:

ActiveSupport::JSON::Encoding.use_standard_json_time_format = true
ActiveSupport::JSON::Encoding.time_precision = 0

If you're not using ActiveSupport, you'll need to use #iso8601 method to convert your Time/DateTime instances to strings before passing them to a QboApi instance. Failure to do so will result in a raised QboApi::BadRequest exception.

Configuration options

Create

  invoice = {
    "Line": [
      {
        "Amount": 100.00,
        "DetailType": "SalesItemLineDetail",
        "SalesItemLineDetail": {
          "ItemRef": {
            "value": "1",
            "name": "Services"
          }
        }
      }
    ],
    "CustomerRef": {
      "value": "1"
    }
  }
  response = qbo_api.create(:invoice, payload: invoice)
  p response['Id'] # => 65

Update

  customer = {
    DisplayName: 'Jack Doe',
    PrimaryPhone: {
      FreeFormNumber: "(415) 444-1234"
    }
  }
  response = qbo_api.update(:customer, id: 60, payload: customer)
  p response.fetch('PrimaryPhone').fetch('FreeFormNumber') # => "(415) 444-1234"

Delete (only works for transaction entities)

  response = qbo_api.delete(:invoice, id: 145)
  p response['status'] # => "Deleted"

NOTE: If you are deleting a journal entry you have to use the following syntax with the underscore, even though this is inconsistent with how you create journal entries.

  response = qbo_api.delete(:journal_entry, id: 145)
  p response['status'] # => "Deleted"

Deactivate (only works for name list entities)

  response = qbo_api.deactivate(:employee, id: 55)
  p response['Active'] # => false

Get an entity by its id

  response = qbo_api.get(:customer, 5)
  p response['DisplayName'] # => "Dukes Basketball Camp"

Get an entity by one of its filter attributes

  response = qbo_api.get(:customer, ["DisplayName", "Dukes Basketball Camp"])
  p response['Id'] # => 5

Get an entity by one of its filter attributes using a LIKE search

  response = qbo_api.get(:customer, ["DisplayName", "LIKE", "Dukes%"])
  p response['Id'] # => 5

Get an entity by one of its filter attributes using a IN search

  response = qbo_api.get(:vendor, ["DisplayName", "IN", "(true, false)"])
  p response.size # => 28

Import/retrieve all

Note: There is some overlap with the all and the get methods. The get method is limited to 1000 results where the all method will return all the results no matter the number.

  # retrieves all active customers
  qbo_api.all(:customers).each do |c|
    p "#{c['Id']} #{c['DisplayName']}"
  end

  # retrieves all active or inactive employees
  qbo_api.all(:employees, inactive: true).each do |e|
    p "#{e['Id']} #{e['DisplayName']}"
  end

  # retrieves all vendors by groups of 5
  qbo_api.all(:vendor, max: 5).each do |v|
    p v['DisplayName']
  end

  # retrieves all customers by groups of 2 using a custom select query
  where = "WHERE Id IN ('5', '6', '7', '8', '9', '10')"
  qbo_api.all(:customer, max: 2, select: "SELECT * FROM Customer #{where}").each do |c|
    p c['DisplayName']
  end

Note: .all() returns a Ruby Enumerator

api.all(:clients).take(50).each { |c| p c["Id"] }
api.all(:clients).count
api.all(:clients).first
api.all(:clients).to_a

Search with irregular characters

  # Use the .esc() method
  name = qbo_api.esc "Amy's Bird Sanctuary"
  response = qbo_api.query(%{SELECT * FROM Customer WHERE DisplayName = '#{name}'})
  # OR USE .get() method, which will automatically escape
  response = qbo_api.get(:customer, ["DisplayName", "Amy's Bird Sanctuary"])
  p response['Id'] # => 1

Email a transaction entity

api.send_invoice(invoice_id: 1, email_address: 'billy@joe.com')

Uploading an attachment

  payload = {"AttachableRef":
              [
                {"EntityRef":
                  {
                    "type": "Invoice",
                    "value": "111"
                  }
                }
              ],
             "FileName": "test.txt",
             "ContentType": "text/plain"
            }
  # `attachment` can be either an IO stream or string path to a local file
  response = qbo_api.upload_attachment(payload: payload, attachment: '/tmp/test.txt')
  p response['Id'] # => 5000000000000091308

Be aware that any errors will not raise a QboApi::Error, but will be returned in the following format:

  {"AttachableResponse"=>
    [{"Fault"=>
       {"Error"=>
         [{"Message"=>"Object Not Found",
           "Detail"=>
            "Object Not Found : Something you're trying to use has been made inactive. Check the fields with accounts, customers, items, vendors or employees.",
           "code"=>"610",
           "element"=>""}],
        "type"=>"ValidationFault"}}],
   "time"=>"2018-01-03T13:06:31.406-08:00"}

Change data capture (CDC) query

  response = qbo_api.cdc(entities: 'estimate', changed_since: '2011-10-10T09:00:00-07:00')
  # You can also send in a Time object e.g. changed_since: Time.now
  expect(response['CDCResponse'].size).to eq 1
  ids = response['CDCResponse'][0]['QueryResponse'][0]['Estimate'].collect{ |e| e['Id'] }
  p ids

Batch operations (limit 30 operations in 1 batch request)

  payload = {
      "BatchItemRequest":
      [
        {
          "bId": "bid1",
          "operation": "create",
          "Vendor": {
            "DisplayName": "Smith Family Store"
          }
        }, {
          "bId": "bid2",
          "operation": "delete",
          "Invoice": {
            "Id": "129",
            "SyncToken": "0"
          }
        }
      ]
  }
  response = qbo_api.batch(payload)
  expect(response['BatchItemResponse'].size).to eq 2
  expect(batch_response.detect{ |b| b["bId"] == "bid1" }["Vendor"]["DisplayName"]).to eq "Smith Family Store"

Reports

        params = { start_date: '2015-01-01', end_date: '2015-07-31', customer: 1, summarize_column_by: 'Customers' }
        response = qbo_api.reports(name: 'ProfitAndLoss', params: params)
        p response["Header"]["ReportName"]) #=> 'ProfitAndLoss'

Reconnect

See docs

        response = qbo_api.reconnect
        #=> if response['ErrorCode'] == 0
        #=>   p response['OAuthToken'] #=> rewq23423424afadsdfs==
        #=>   p response['OAuthTokenSecret'] #=> ertwwetu12345312005343453yy=Fg

Disconnect

See docs

        response = qbo_api.disconnect
        #=> if response['ErrorCode'] == 0
        #=>   # Successful disconnect

Respond to an error

  customer = { DisplayName: 'Weiskopf Consulting' }
  begin
    response = qbo_api.create(:customer, payload: customer)
  rescue QboApi::BadRequest => e
    if e.message =~ /Another customer already exists with this name/
      # Query for Id using DisplayName
      # Do an qbo_api.update instead
    end
  end

What kind of QuickBooks entity?

  p qbo_api.is_transaction_entity?(:invoice) # => true
  # Plural is supported as well
  p qbo_api.is_transaction_entity?(:invoices) # => true
  p qbo_api.is_transaction_entity?(:customer) # => false
  p qbo_api.is_name_list_entity?(:vendors) # => true

Download Quickbooks PDF

A quickbooks supplied PDF can be downloaded for api endpoints which offer a PDF.

qbo_api.get_pdf(:invoice, 121) # produces a pdf stream.

The PDF stream can then be saved using your preferred method.

# example using Ruby on Rails with ActiveStorage
class Invoice
  has_one_attached :pdf_file
end
invoice_number = 121
pdf_data = qbo_api.get_pdf(:invoice, invoice_numer) # returns raw pdf stream
pdf_io = StringIO.new(pdf_data) # convert to a StringIO object
filename = "invoice_no_#{invoice_number}"

invoice.pdf_file.attach(
    io: pdf_io,
    filename: filename,
    content_type: 'application/pdf'
)
# plain ruby example
invoice_number = 121
pdf_data = qbo_api.get_pdf(:invoice, invoice_numer) # returns raw pdf stream
filename = "invoice_no_#{invoice_number}.pdf"

File.write(filename, pdf_data)

Spin up an example

Webhooks

See https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html for how to install ngrok and what it is.

Just For Hackers

raw_response = connection.post do |req| req.body = { grant_type: :refresh_token, refresh_token: current_refresh_token } req.url '/oauth2/v1/tokens/bearer' end

- Once your .env file is completely filled out you can use the console to play around in your sandbox

bin/console test

@qbo_api.get :customer, 1

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/minimul/qbo_api.

Running the specs

Creating new specs or modifying existing spec that have been recorded using the VCR gem.

License

The gem is available as open source under the terms of the MIT License.