Git Product home page Git Product logo

qbo_api's Introduction

QboApi

Ruby client for the QuickBooks Online API version 3.

  • JSON only support.
    • Please don't ask about XML support. Intuit has stated that JSON is the primary data format for the QuickBooks API (v3 and beyond). This gem will specialize in JSON only. The quickbooks-ruby gem has fantastic support for those who favor XML.
  • Features specs built directly against a QuickBooks Online Sandbox via the VCR gem.
  • Robust error handling.

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 [email protected]: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

  • By default this client runs against a QBO sandbox. To run against the production QBO API URL do:
QboApi.production = true
  • Logging:
QboApi.log = true
  • To change logging target from $stdout e.g.
QboApi.logger = Rails.logger
QboApi.request_id = true
  • To run individual requests with a RequestId then do something like this:
  resp = qbo_api.create(:bill, payload: bill_hash, params: { requestid: qbo_api.uuid })
  # Works with .get, .create, .update, .query methods
  • By default, this client runs against the current "base" or major version of the QBO API. This client does not run against the latest QBO API minor version by default. To run all requests with a specific minor version, you must specify it:
QboApi.minor_version = 8
  • To run individual requests with a minor version then do something like this:
  resp = qbo_api.get(:item, 8, params: { minorversion: 8 })
  # Works with .get, .create, .update, .query methods

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: '[email protected]')

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

Spin up an example

  • Check out this article on spinning up the example.

  • Create a Intuit developer account at https://developer.intuit.com

  • Create an app in the intuit developer backend

    • Go to myapps
    • Create an app with both the Accounting & Payments selected.
  • Setup the app and gather credentials

    • Go to the Development Dashboard
    • Click on your App name
    • Click "Keys & credentials" under Development Settings in left menu
    • Copy the 'Client ID' and the 'Client Secret'
    • Add a Redirect URI: http://localhost:9393/oauth2-redirect (or whatever PORT= is in your .env)
    • Create a new Sandbox Company by clicking on "View Sandbox companies" Don't use it for anything else besides testing this app.
    • Copy the 'Company ID' for use later
      1. Within 'Sandbox Company_US_1' click on the COG -> Additional info
      2. Copy the 'Company ID'
  • Setup qbo_api

    • git clone git://github.com/minimul/qbo_api && cd qbo_api
    • bundle
  • Create a .env file

    • cp .env.example_app.oauth2 .env
    • Edit your .env and enter the following
      export QBO_API_CLIENT_ID=[YOUR CLIENT ID]
      export QBO_API_CLIENT_SECRET=[YOUR CLIENT SECRET]
      export QBO_API_COMPANY_ID=[YOUR COMPANY ID]
      
  • Start up the example app and test

    • ruby example/oauth2.rb
    • Go to http://localhost:9393/oauth2
    • Use the Connect to QuickBooks button to connect to your QuickBooks sandbox, which you receive when signing up at https://developer.intuit.com.
    • After successfully connecting to your sandbox go to http://localhost:9393/oauth2/customer/5
    • You should see "Dukes Basketball Camp" displayed
  • Checkout example/oauth2.rb to see what is going on under the hood.

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.

  • With the example app running, run: ngrok http 9393 -subdomain=somereasonablyuniquenamehere

  • Go to the Development tab

  • Add a webhook, Select all triggers and enter the https url from the ngrok output https://somereasonablyuniquenamehere/webhooks

  • After saving the webhook, click 'show token'. Add the token to your .env as QBO_API_VERIFIER_TOKEN

  • In another tab, create a customer via the API: bundle exec ruby -rqbo_api -rdotenv -e 'Dotenv.load; p QboApi.new(access_token: ENV.fetch("QBO_API_ACCESS_TOKEN"), realm_id: ENV.fetch("QBO_API_COMPANY_ID")).create(:customer, payload: { DisplayName: "TestCustomer" })' (You'll also need to have added the QBO_API_COMPANY_ID and QBO_API_ACCESS_TOKEN to your .env)

    There could be a delay of up to a minute before the webhook fires.

    It'll appear in your logs like:

    {"eventNotifications"=>[{"realmId"=>"XXXX", "dataChangeEvent"=>{"entities"=>[{"name"=>"Customer", "id"=>"62", "operation"=>"Create", "lastUpdated"=>"2018-04-08T04:14:39.000Z"}]}}]}
    Verified: true
    "POST /webhooks HTTP/1.1" 200 - 0.0013
    

Just For Hackers

  • Using the build_connection method
connection = build_connection('https://oauth.platform.intuit.com', headers: { 'Accept' => 'application/json' }) do |conn|
  conn.basic_auth(client_id, client_secret)
  conn.request :url_encoded # application/x-www-form-urlencoded
  conn.response :json
  conn.use QboApi::RaiseHttpException
end

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.

qbo_api's People

Contributors

ayaman avatar bf4 avatar dependabot[bot] avatar franklin-stripe avatar gmhawash avatar graudeejs avatar map7 avatar mculp avatar minimul avatar ntippie avatar phildionne avatar saboter avatar ybakos avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

qbo_api's Issues

"Get" seems to be throwing a debug notification?

When I execute a get request for a purchase order, I'm getting the following in my console.
I didn't write it so I'm guessing its coming from the gem somewhere?

It looks like what's happening is that entity_name is set to "Purchaseorder" instead of "PurchaseOrder"?

D, [2018-10-09T10:39:45.808334 #74262] DEBUG -- : [QuickBooks] entity name not in response body: entity="PurchaseOrder" entity_name="Purchaseorder" body={"PurchaseOrder"=>{"ShipAddr"=>

Sinatra server handler not found error with Ruby 3.0.1

tldr: I was unable to spin up the example app under Ruby 3, but able to run it under 2.7.2. I'm not certain of the root cause.

In all failure cases, the error was this:

/home/ken/.asdf/installs/ruby/3.0.1/lib/ruby/gems/3.0.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1755:in detect_rack_handler': Server handler (thin,puma,reel,HTTP,webrick) not found. (RuntimeE rror) from /home/ken/.asdf/installs/ruby/3.0.1/lib/ruby/gems/3.0.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1493:in run!'
from /home/ken/.asdf/installs/ruby/3.0.1/lib/ruby/gems/3.0.0/gems/sinatra-contrib-2.1.0/lib/sinatra/reloader.rb:260:in run!' from example/oauth2.rb:82:in

'

I read that WEBrick was removed from 3.0, but adding the gem to the Gemfile failed with the same error, as did Shotgun and Puma. As stated, downgrading to 2.7.2 allowed me to start and successfully complete the oauth flow without needing to provide or specify an app server.

I'd like to use this api in a Rails 6 app running Ruby 3. I'm willing to help but am not very familiar with Sinatra or how Rack decides which handler to use, and trying to provide an app server pretty much exhausted my initial ideas. If you know what's wrong and it's an easy fix, thanks. :) If not, I'd appreciate any guidance you could provide.

api auth errors are now different

As evidenced by

NoMethodError: undefined method `[]' for nil:NilClass"
["/app/vendor/bundle/ruby/2.3.0/gems/qbo_api-1.5.3/lib/qbo_api/raise_http_exception.rb:55:in `parse_json'","/app/vendor/bundle/ruby/2.3.0/gems/qbo_api-1.5.3/lib/qbo_api/raise_http_exception.rb:47:in `error_body'","/app/vendor/bundle/ruby/2.3.0/gems/qbo_api-1.5.3/lib/qbo_api/raise_http_exception.rb:41:in `error_message'","/app/vendor/bundle/ruby/2.3.0/gems/qbo_api-1.5.3/lib/qbo_api/raise_http_exception.rb:15:in `block in call'","/app/vendor/bundle/ruby/2.3.0/gems/faraday-0.13.1/lib/faraday/response.rb:61:in `on_complete'","/app/vendor/bundle/ruby/2.3.0/gems/qbo_api-1.5.3/lib/qbo_api/raise_http_exception.rb:9:in `call'
]

Notice that the element names change from Fault -> fault etc. Also, technically, a request with Accept: application/json should return a 415 rather than Content-Type: text/xml.

 - request:
      method: get
    uri: https://sandbox-quickbooks.api.intuit.com/v3/company/XXXXX/query?query=SELECT%20*%20FROM%20Account%20MAXRESULTS%201%20STARTPOSITION%201
     headers:
       Content-Type:
       - application/json;charset=UTF-8
       Accept:
       - application/json
   response:
     headers:
       Content-Type:
-      - text/xml
+      - application/json
     body:
        encoding: UTF-8
-      string: |
-        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-        <IntuitResponse time="2017-10-30T07:25:42.838-07:00" xmlns="http://schema.intuit.com/finance/v3">
-            <Fault type="AUTHENTICATION">
-                <Error code="3200">
-                    <Message>message=AuthenticationFailed; errorCode=003200; statusCode=401</Message>
-                    <Detail></Detail>
-                </Error>
-            </Fault>
-        </IntuitResponse>
+      string: '{"warnings":null,"intuitObject":null,"fault":{"error":[{"message":"message=AuthenticationFailed;
+        errorCode=003200; statusCode=401","detail":"","code":"3200","element":null}],"type":"AUTHENTICATION"},"report":null,"queryResponse":null,"batchItemResponse":[],"attachableResponse":[],"syncErrorResponse":null,"requestId":null,"time":1512143684083,"status":null,"cdcresponse":[]}'

I've hacked around it by patching the middleware

require 'qbo_api/raise_http_exception'
module QboApiInconsistentJsonResponseHack
  # Expected Structure:
  # {"Fault":{"Error":[{"Message":"message=AuthenticationFailed; errorCode=003200; statusCode=401",
  #                    "Detail":"",
  #                     "code":"3200","element":""}],
  #                     "type":"AUTHENTICATION"},"time":"2017-11-16T13:48:17.050-08:00"}'
  # fails to be parsed when now returned with different keys
  # {"warnings":null,"intuitObject":null,
  #  "fault":{"error":[{"message":"message=AuthenticationFailed; errorCode=003200; statusCode=401",
  #                     "detail":"","code":"3200","element":null}],
  #                   "type":"AUTHENTICATION"},"report":null,"queryResponse":null,"batchItemResponse":[],"attachableResponse":[],"syncErrorResponse":null,"requestId":null,"time":1512105735900,"status":null,"cdcresponse":[]}
  # usually when was previously XML
  #   <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  #   <IntuitResponse time="2017-10-30T07:25:42.838-07:00" xmlns="http://schema.intuit.com/finance/v3">
  #       <Fault type="AUTHENTICATION">
  #           <Error code="3200">
  #               <Message>message=AuthenticationFailed; errorCode=003200; statusCode=401</Message>
  #               <Detail></Detail>
  #           </Error>
  #       </Fault>
  #   </IntuitResponse>
  def parse_json(body)
    return super unless body =~ /"fault"/
    res = JSON.parse(body)
    # keys different from expected
    raise unless res.key?("fault")
    fault = res["fault"]
    raise unless (["error", "type"] - fault.keys).empty?
    errors = fault["error"]
    errors.map { |error|
      raise unless (["code", "message", "detail"] - error.keys).empty?
      {
        fault_type: fault["type"],
        error_code: error['code'],
        error_message: error['message'],
        error_detail: error['detail']
      }
    }
  end
end
FaradayMiddleware::RaiseHttpException.prepend(QboApiInconsistentJsonResponseHack)

Error in initiatialize and cdc

The following line were giving me issues when trying to run the example:

 def initialize(token:, token_secret:, realm_id:, consumer_key: CONSUMER_KEY, 
                 consumer_secret: CONSUMER_SECRET, endpoint: :accounting)

I got it to work by changing it as such:

 def initialize(token: '', token_secret: '', realm_id: '', consumer_key: CONSUMER_KEY, 
                 consumer_secret: CONSUMER_SECRET, endpoint: :accounting)

There were a few more lines with the same issues. Basically passing empty values as default values.

cdc ... update ... create ... delete

Can't convert Array to String (Array#to_str gives Array) (TypeError)

I'm using rails 7.0.1 and ruby 3.0.2

Assuming I use the following example(token and realm redacted):

QboApi.production = false
qbo_api = QboApi.new(access_token: "qyprd2uvCOdRq8xzoSSiiiiii", realm_id: "12314xxxxxx7")
qbo_api.get :customer, 1

I get the following error:

/Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:663:in `to_s': can't convert Array to String (Array#to_str gives Array) (TypeError)

To test the array.to_s method I was able to run the following in the next line:
['test', 'array', 'goes', 'here'].to_s
which executed as expected.

I'm only getting this error when I try to use the qbo_api gem, I've never seen this error otherwise. I'm able to get a token from Quickbooks Oauth api, and I'm able to create a session, but when I try to interact with any datasets I get this error.

Here's the full output of the error

/Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:663:in `to_s': can't convert Array to String (Array#to_str gives Array) (TypeError)
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:663:in `message'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:663:in `full_message'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:663:in `handle_exception'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:607:in `block (2 levels) in eval_input'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:777:in `signal_status'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:567:in `block in eval_input'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb/ruby-lex.rb:267:in `block (2 levels) in each_top_level_statement'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb/ruby-lex.rb:249:in `loop'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb/ruby-lex.rb:249:in `block in each_top_level_statement'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb/ruby-lex.rb:248:in `catch'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb/ruby-lex.rb:248:in `each_top_level_statement'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:566:in `eval_input'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:500:in `block in run'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:499:in `catch'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:499:in `run'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/irb-1.6.2/lib/irb.rb:421:in `start'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/railties-7.0.4/lib/rails/commands/console/console_command.rb:70:in `start'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/railties-7.0.4/lib/rails/commands/console/console_command.rb:19:in `start'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/railties-7.0.4/lib/rails/commands/console/console_command.rb:102:in `perform'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/thor-1.2.1/lib/thor/command.rb:27:in `run'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/thor-1.2.1/lib/thor/invocation.rb:127:in `invoke_command'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/thor-1.2.1/lib/thor.rb:392:in `dispatch'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/railties-7.0.4/lib/rails/command/base.rb:87:in `perform'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/railties-7.0.4/lib/rails/command.rb:48:in `invoke'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/railties-7.0.4/lib/rails/commands.rb:18:in `<main>'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/bootsnap-1.15.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
from /Users/user_name/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/bootsnap-1.15.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'

Tax code issues

Hi,

I'm using this gem to integrate quickbooks with my product. I'm able to sync invoice and it's line items to quickbooks and I've also figured out a way that how to apply tax code on invoice line items but I'm facing an issue when I try to apply tax rate to a quickbooks invoice. Can anyone please guide me to some tutorial or help me by telling me about what methods I should use to apply the tax-rate.

And alo, let me know there is a way to integrate custom taxes to invoices.

Thanks.

No revoke method?

According to the Intuit docs, it looks like its required to provide users of Appstore apps to provide a mechanism for users to revoke permission for the app to access the qb company. It doesn't look the like the "disconnect" api method (which returns an error when I try it) achieves what intuit requires. It seems to require a post to a intuit url ending in "revoke" which I don't see.

Any chance of putting that in the api? You can't really publish anything via Intuit without it.

Library is not using latest API base version / minor version

According to the QBO API documentation, when we do not specify a minor version, the API will behave according to the current "base version" which is the latest minor version. That is 65, at the time of this writing.

However, when QboApi.minor_version is false, I am not observing behavior present in the latest minor version on the QBO side.

For example, when I query :item, I expect to see Sku in the response payload. But it is not present. If I specify the QboApi.minor_version as 4, when the Item.Sku property was introduced, then I do see Sku in the response.

Similarly, when I specify the QboApi.minor_version as 65, I also see Sku in the response.

Shouldn't the qbo_api gem be using the most recent minor version by default? It is not. What is the default minor version?

Can we document what this default minor version is in the README?

Weird response from .all(:creditmemo)

#all(:creditmemo) returns the wrong structure, so the enumerator doesn't work. When I use the API explorer to query "select * from creditmemo" it's got the same structure as other things e.g "select * from invoice" but by the time it percolates up to the enumerator the structure is different so the enumerator just enumerates the outside, not the inner entities. For example;-

This works

api.all(:bill,max: 1).each{|k| ap k; break}

and returns

{
        "SalesTermRef" => {
    "value" => "5"
},
             "DueDate" => "2018-02-27",
 &etc

I.e the structure of the first entity, as it should be
but this doesn't work

api.all(:creditmemo,max: 1).each{|k| ap k; break}

and returns this

[
[0] "QueryResponse",
[1] {
       "CreditMemo" => [
        [0] {
                 "RemainingCredit" => 0,
                          "domain" => "QBO",
                          "sparse" => false,
                              "Id" => "5728",
     &etc

It's returning the outer structure with the entities buried deep inside it.

But when I look at the response from the api explorer

 select * from bill

returns

  {

"QueryResponse": {
"Bill": [
{
"SalesTermRef": {
"value": "5"
},
"DueDate": "2018-02-27",

And

  select * from creditmemo

returns

{

"QueryResponse": {
"CreditMemo": [
{
"RemainingCredit": 0,
"domain": "QBO",
"sparse": false,
"Id": "5728",

Which is the same structure, Outer "QueryResponse", inner "CreditMemo": which is an array of creditmemos. But somehow this is getting interpreted as an array

  [
[0] "QueryResponse",
[1] {
       "CreditMemo" => [

Reading through the code I can't see any reason why they should be different, and everything else, items, terms, invoices, is OK. It's only with creditmemos that it does weird stuff and there's no line of code parse_it_wrong if entity == 'creditmemo' . So I'm mystified.

Instructions for enabling item categories

Sharing here since I'm not sure how to post an answer to an unasked question on the intuit site

tl;dr

to enable item categories in dev, when logged in to a sandbox company, go to https://sandbox.qbo.intuit.com/app/categorymigration and click 'Switch to Categories'

Details

This is a new dev account using the OAuth 2.0 API

I was unable to turn on categories in the sandbox by updating companyinfo or any obvious page in the web ui.

In prod, I noticed that the URL for the categories page was /app/category, so I went to https://sandbox.qbo.intuit.com/app/categories and it redirected to https://sandbox.qbo.intuit.com/app/categorymigration and I clicked the link to 'Switch to Categories'. I was then able to go to /app/categories in the web ui to create categories, as well as create them via the API.

It appears the migration posted a CORS request to https://sandbox.qbo.intuit.com/qbo50/neo/v1/company/193514655268479/companysetup/enableCategories
e.g.

POST /item, payload: { Name: 'Transportation', Type: 'Category' }, params: { minorversion: 4} 

Before the enablecategories migration, the API returned I got a 400

[{:fault_type=>"ValidationFault", :error_code=>"500", :error_message=>"Unsupported Operation", :error_detail=>"Operation CREATE CATEGORY is not supported."}] 

afterwards

{"Name"=>"Flowers", "Active"=>true, "FullyQualifiedName"=>"Transportation", "Type"=>"Category", "domain"=>"QBO", "sparse"=>false, "Id"=>"36", "SyncToken"=>"0", "MetaData"=>{"CreateTime"=>"2017-11-08T08:58:38-08:00", "LastUpdatedTime"=>"2017-11-08T08:58:38-08:00"}}

Not a bug: Category of Type: Service

Weird note: a subsequent query returned with Type: Service instead of Type: Category. (Quickbooks support suggested I use a minor version 12 or greater, but I didn't go through the trouble to create a new company to check that)

{"Name"=>"Transportation", "Active"=>true, "FullyQualifiedName"=>"Transportation", "Type"=>"Service", "domain"=>"QBO", "sparse"=>false, "Id"=>"36", "SyncToken"=>"0", "MetaData"=>{"CreateTime"=>"2017-11-08T13:28:16-08:00", "LastUpdatedTime"=>"2017-11-08T13:31:39-08:00"} 

but I was still able to use it as a super category,

e.g.

 { "Name"=>"Hour", "Active"=>true, "SubItem"=>true, "ParentRef"=>{"value"=>"36", "name"=>"Transportation"}, "Level"=>1, "FullyQualifiedName"=>"Transportation:Hour", "Type"=>"Service", "domain"=>"QBO", "sparse"=>false, "Id"=>"37", "SyncToken"=>"0", "MetaData"=>{"CreateTime"=>"2017-11-08T13:31:18-08:00", "LastUpdatedTime"=>"2017-11-08T13:31:18-08:00"}} 

Recurring invoices

I had a thought about recurring invoices (invoice every month).
Since there is no recurring invoice in the API, I was thinking about implementing a function (using qbo API) that gets a list of customers, extract the last sent invoice, and if month has passed, send a new one.

What do you think?
I'm thinking about implementing it my self.

I'm not an experienced Ruby developer, but I would like to try.

Is the "OAuth2: Spin up an example" current?

"Create a new Company (from the manage sandboxes page) Don't use it for anything else besides testing this"

I'm getting stuck on this step. The "manage sandboxes page" does not go to a "manage sandboxes page".

thanks!

Noticed some code you don't need

Just downloaded your gem and started reading through it. I noticed you've got a method

 def snake_to_camel(sym)

But ActiveSupport already has .camelize which does the job for you.
See ActiveSupport and look at section 5.11.3

You can remove that method.

I also noticed there's a method to parse XML, but I thought the whole idea of this gem was to get away from the old XML api. I'm only just getting in to the QBO API, is there still some XML hanging around in the latest API?

Skip already imported vendors

I am trying something like this:

query = "SELECT * FROM Vendor"
query += " WHERE Id NOT IN (#{already_imported_ids.join(', ')})"
# => "SELECT * FROM Vendor WHERE Id NOT IN (1, 2, 3, 4 , 5, 6)"
qbo_api.all(:vendor, select: query).each do |vendor|
  p vendor['Id']
end

But I am getting Bad request error:

QboApi::BadRequest: [{:fault_type=>nil, :error_code=>"4000", :error_message=>"Error parsing query", :error_detail=>"QueryParserError: Encountered \" <INTEGER> \"1 \"\" at line 1, column 39.\nWas expecting one of:\n    \"false\" ...\n    \"true\" ...\n    <DATETIME> ...\n    <ID> ...\n    <STRING> ...\n    "}]
from /Users/przbadu/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/qbo_api-1.5.1/lib/qbo_api/raise_http_exception.rb:13:in `block in call'

How to properly pass those ids?

application/pdf?

Using this gem in earnest now. It's really useful and I really appreciate the work you've done on it. I see you've even done a method to get the reports, which is a nice touch.

But I'm missing a method to get invoices as pdf files. I see from the documentation that it's a simple get request with the invoice id, but I have to set the Content Type = 'application/pdf' and there doesn't seem to be an easy way to do that without adding some extra parameters to the method calls.

Any thoughts?

QboApi::COMSUMER_KEY and QboApi::CONSUMER_SECRET not being read as defaults

The method inheritance for QboApi::Connection::OAuth1#default_attributes isn't working, since included modules don't supersede or overwrite existing methods. I have a commit coming shortly that will fix this by using prepend Connection instead of include Connection, but also want to throw it over to @bf4 in case you want to solve it in another manner.

Error 22 Authentication required on disconnect with OAuth2

When trying to disconnect my QboApi client I get:

client.disconnect
{"ErrorMessage"=>"Authentication required", "ErrorCode"=>22, "ServerTime"=>"/Date(1511453624425)/"}

The other functionalities, like client.all(:company_info).entries.to_a work normally. What is more I am almost sure disconnect worked before too. What can I do to have this client disconnected?

Issue querying all records

When running:

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

The following error is returned in the console:

LocalJumpError: no block given (yield)

#deactivate does not work properly for Account

If you try to deactivate an account, it will raise the following error:

QboApi::BadRequest: 
[{:fault_type=>"ValidationFault", 
:error_code=>"2020", 
:error_message=>"Required param missing, need to supply the required value for the API", 
:error_detail=>"Required parameter Name is missing in the request"}]

How do you think it would best be fixed? Using the full object (rather than sparse) for the update seems like an easy solution, but that may open up a number of other issues since there are many name list entities, and some fields act uniquely around updates (and sometimes their behavior changes according to settings, like Payroll being enabled). We could just change #set_update to include Name or DisplayName if they are present in the read object.

Sending an Invoice

I believe I've looked through all of the documentation. Is there a way to send an invoice using this gem? I have tried sending an invoice on the create method with this payload:

BillEmail: { Address: "[email protected]" }, EmailStatus: "EmailSent", DeliveryInfo: { DeliveryType: "Email", DeliveryTime: DateTime.now.to_s },

"All" method parameter "inactive" seems to be broken

Hello,

according to docs the inactive param set to true should return both active and inactive entity records:

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

Current default is inactive = false. Which kind of indicates that it will return only active records.
Currently both active and inactive records are returned when the inactive param is set to false (tested on TaxCode). Which looks little bit like a change in default QBO behavior.

def build_all_query(entity, select: nil, inactive: false)
      select ||= "SELECT * FROM #{singular(entity)}"
      select += join_or_start_where_clause!(select: select) + 'Active IN ( true, false )' if inactive
      select
end

If the inactive param should behave as expected it should always add condition e.g.:

def build_all_query(entity, select: nil, inactive: false)
      select ||= "SELECT * FROM #{singular(entity)}"
      active_condition =  inactive ? 'Active IN ( true, false )' : 'Active = true'
      select += join_or_start_where_clause!(select: select) + active_condition
      select
end

Thank you.

Faraday::TimeoutError - Net::ReadTimeout and request_id

I've been running along smoothly for a few weeks now, coding in earnest, and suddenly today I'm getting loads of timeout errors, which is a nuisance as I'm writing and deleting, so not getting a response is a big issue.

I see in the QBO documentation that if I add requestid parameter to the path then I can send the request again and it'll be idempotent. I also see in the QboAPI code that if I set QboApi.request_id = true then it'll add a request id to the path. But...

There's no way to resend the request without triggering a brand new call to finaliaize_path which will call uuid again and get a new uuid so the requestid parameter will be different each time. That means each POST will be treated as new, and I'll be creating say invoices with the same data over again. It's not idempotent.

without changing the code in the gem, how can I resend a request with the same request_id ?

Correct way to handle attachments?

Our app needs to manipulate attachable objects, doesn't look like support for file upload exists today.

Before I put together a pull request I wanted to see if there was any thoughts as to how to implement it.

The main difference is the faraday request needs to be multipart form rather than a json request and the json request that goes along with the file goes as a separate file attachment rather than as JSON in the body. The paths for file operations are also different than the standard REST endpoints. It's quite a bit different than every other request/response from the server for other types.

I figure it's prudent to fit it in to the existing pattern as much as possible but I could also see using different methods to work with it - is there any preference?

I put together a quick module hack that successfully uploads files and generally fits in the same api.method(:resource, payload: payload) pattern with the exception of needing to pass in a file hash param for the file to be uploaded.

module QboAttachments
  def connection(url: get_endpoint, multipart: false)
    Faraday.new(url: url) do |faraday|
      faraday.headers['Content-Type'] = 'application/json;charset=UTF-8' unless multipart
      faraday.headers['Accept'] = 'application/json'
      puts 'SETTING MULTIPART' if multipart
      if !@token.nil?
        faraday.request :oauth, oauth_data
      elsif !@access_token.nil?
        faraday.request :oauth2, @access_token, token_type: 'bearer'
      else
        raise QboApi::Error.new error_body: 'Must set either the token or access_token'
      end
      faraday.request :multipart if multipart
      faraday.request :url_encoded
      faraday.use FaradayMiddleware::RaiseHttpException
      faraday.response :detailed_logger, QboApi.logger if QboApi.log
      faraday.adapter  Faraday.default_adapter
    end
  end

  def request(method, path:, entity: nil, payload: nil, params: nil)
    multipart = entity == :attachable
    raw_response = connection(multipart: multipart).send(method) do |req|
      path = finalize_path(path, method: method, params: params)
      case method
      when :get, :delete
        req.url path
      when :post, :put
        req.url path
        if multipart
          req.url finalize_path(path, method: method, params: params).gsub('attachable', 'upload')
          req.body = { file_metadata_01: Faraday::UploadIO.new(StringIO.new(JSON.generate(payload.except('file'))), 'application/json', 'attachment.json'),
                       file_content_01: payload['file'] }
        else
          req.body JSON.generate(payload)
        end
      end
    end
    response(raw_response, entity: entity)
  end
end
QboApi.prepend(QboAttachments)

> api = QboApi.new(...)
> payload = { AttachableRef: [{ EntityRef: {type: 'Invoice', value: '9'}, IncludeOnSend: 'true' }], FileName: 'test.txt', ContentType: 'text/plain', file: Faraday::UploadIO.new('test.txt', 'text/plain') }
> api.create(:attachable, payload: payload)
(returns an AttachableResponse and invoice 9 now has a visible attachment of test.txt on it)

Thank you

Instructions for getting started with the example app no longer work

I think the instructions for starting the example app haven't kept up to date with the changes in QBO. When I follow the instruction in this link Part 1 they refer to Oauth Consumer Key, and Oauth Consumer Secret. But the latest QBO app page only has 'Client ID' and 'Client Secret'

I tried using the values of Client ID in place of Oauth Consumer Key, and Client Secret in Oauth Consumer Secret. But that gave an error
screen shot 2018-01-04 at 17 03 36

Also the instructions show only a button on the home page of the example app. But in the actual app there's a link to Oauth2 connect page. But if I follow that link then I get this error

screen shot 2018-01-04 at 17 59 31

So, next try, read the code. I see in the code

 CLIENT_ID = ENV['QBO_API_CLIENT_ID']
 CLIENT_SECRET = ENV['QBO_API_CLIENT_SECRET']

Ahhh! This is not mentioned in the instructions.

So in my .env file I added QBO_API_CLIENT_ID and QBO_API_CLIENT_SECRET using the values for Client ID, and Client Secret and removed Oauth Consumer Key & etc

Then ...

On the home page of the app, the button fails with

screen shot 2018-01-04 at 18 16 16

But the link to Oauth2 connect page doesn't get an error! This time I see another page with a single button on it. Clicking the button then gives this error

screen shot 2018-01-04 at 18 18 57

Which is progress of a sort, but obviously the instructions bare no relation to the new reality and so I completely abandoned them. I'm on my own with the source code and error messages to work through. So...

Reading the source I see

 `client = Rack::OAuth2::Client.new(
  identifier: CLIENT_ID,
  secret: CLIENT_SECRET,
  redirect_uri: "http://localhost:#{PORT}/oauth2-redirect",
  authorization_endpoint: "https://appcenter.intuit.com/connect/oauth2",
  token_endpoint: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer"
)

`

Ahh! so I have to set up the redirect URL in QBO. Once I've done that and I restart then I do get redirected to the correct login sequence and can connect the test app to a sandbox. and I get an access token and a refresh token.

Conclusion. The code works but the instructions are completely out of date.

Ampersand in QB query breaks OAuth

Hi,
first of all, thank you for your work on this great gem.

I have run recently into weird problems where some query api requests failing with auth exception. The core of the problem is actually known to you according to this article: http://minimul.com/escaping-an-ampersand-within-an-oauth-request.html was

Current implementation of the request method does a URI encode on whole path and the ampersand character is not escaped/encoded for obvious reason. So at least for query requests this is not ideal approach.

I have solved this issue only with some simple and not very elegant overrides for now. Something like:

class FixedQboApi < QboApi
        def request(method, path:, entity: nil, payload: nil, url_encode: true)
          raw_response = connection.send(method) do |req|
            case method
              when :get, :delete
                p =  url_encode ? URI.encode(path) : path
                req.url p
              when :post, :put
                req.url add_request_id_to(path)
                req.body = JSON.generate(payload)
            end
          end
          response(raw_response, entity: entity)
        end

        def query(query)
          path = "#{realm_id}/query?query=#{CGI.escape(query)}"
          entity = extract_entity_from_query(query, to_sym: true)
          request(:get, entity: entity, path: path, url_encode: false)
        end
end

The url encoding in the request method is optional in the fixed class and query method escapes only parameter not whole path.

Regards,

Martin

Refresh token

Is there any method in this gem that allows me to send a refresh token to QBO and get back a fresh access token?

From what I can see I have to handle 401 errors and create my own method to get a new access token. Since that's a pretty common thing to have to do, it's would make sense if it was part of the gem.

Am I missing something?

OAuth 2.0 requests returning 401 Unauthorized

I'm trying to create a connection to QBO via OAuth 2.0 example you guys have. I get the access token & refresh token then create a connection like so:

api = QboApi.new(access_token: session[:access_token], realm_id: session[:realm_id])

Then making a call like:

api.get :customer, 58

returns me an authentication error:

AuthenticationErrorGeneral: SRV-110-Authentication Failure , statusCode: 401

Even though I can make this exact same call from their API Explorer (yes I've double and triple checked the URL's/Realm ID). I've also double checked the API keys from the settings page and everything matches. It's very strange that I get a proper access token but can make no subsequent calls.

I am using this example:

https://github.com/minimul/qbo_api/blob/master/example/oauth2.rb

Any help? Anyone else able to make OAuth 2.0 calls successfully? (This is for development sandbox)

401 Unauthorized issues after upgrade to qbo_api 3.0

Hi everybody,

I was following closely your efforts on the recent faraday upgrade related PR (#118) and rushed to test the new version.

Unfortunately after upgrade my QBO integration stopped working. I am receiving the same exception like in this old closed issue:

#84

QboApi::Unauthorized: [{:fault_type=>"AuthenticationFault", :error_code=>"100", :error_message=>"General Authentication Error", :error_detail=>"AuthenticationErrorGeneral: SRV-110-Authentication Failure , statusCode: 401"}]

Tokens are good, they were tested with this snippet:

access_token="put the access token in these quotes"
realm_id="put the company id in these quotes"
curl -X GET "https://sandbox-quickbooks.api.intuit.com/v3/company/${realm_id}/companyinfo/${realm_id}" \
 -H "Accept: application/json" \
 -H "Content-Type: application/json;charset=UTF-8" \
 -H "User-Agent: virtuexru  test" \
 -H "Authorization: Bearer ${access_token}"

Gems:

oauth2 = 2.0.9
faraday = 2.7.2

Is the new version working for you also in your production usage? @bf4 @mculp @minimul

Thank you.

Creating Time Activity is not saving correctly on QBO

Hi There,

I'm having this strange issue, I'm creating a time activity however the response back form QBO is different to what was originally created.

time = {

        "NameOf": "Employee",
        "EmployeeRef": {
        "value": "7",
        "name": "Emily Platt"
    },
        "StartTime": "2015-07-05T08:00:00-08:00",
        "EndTime": "2013-07-05T17:00:00-08:00"
    }


    response = @qbo_api.create(:timeactivity, payload: time)
    puts response

The response back from QBO is:

0\n\n{\"TimeActivity\":{\"TxnDate\":\"2016-09-15\",\"NameOf\":\"Other\",\"OtherNameRef\":{\"value\":\"7\",\"name\":\"Freeman Sporting Goods\"},\"ItemRef\":{\"value\":\"2\",\"name\":\"Hours\"},\"BillableStatus\":\"NotBillable\",\"Taxable\":false,\"HourlyRate\":0,\"StartTime\":\"2016-09-15T09:00:00-07:00\",\"EndTime\":\"2016-09-15T18:00:00-07:00\",\"domain\":\"QBO\",\"sparse\":false,\"Id\":\"11\",\"SyncToken\":\"0\",\"MetaData\":{\"CreateTime\":\"2016-09-15T01:11:20-07:00\",\"LastUpdatedTime\":\"2016-09-15T01:11:20-07:00\"}},\"time\":\"2016-09-15T01:11:20.742-07:00\"}"
{"TimeActivity"=>{"TxnDate"=>"2016-09-15", "NameOf"=>"Other", "OtherNameRef"=>{"value"=>"7", "name"=>"Freeman Sporting Goods"}, "ItemRef"=>{"value"=>"2", "name"=>"Hours"}, "BillableStatus"=>"NotBillable", "Taxable"=>false, "HourlyRate"=>0, "StartTime"=>"2016-09-15T09:00:00-07:00", "EndTime"=>"2016-09-15T18:00:00-07:00", "domain"=>"QBO", "sparse"=>false, "Id"=>"11", "SyncToken"=>"0", "MetaData"=>{"CreateTime"=>"2016-09-15T01:11:20-07:00", "LastUpdatedTime"=>"2016-09-15T01:11:20-07:00"}}, "time"=>"2016-09-15T01:11:20.742-07:00"}

Ive tried using the API explorer within QBO and it seems to creating time activity records correctly.

Any help would be greatly appreciated!

Time serialization

Some objects that use datetimes (Time or DateTime classes in Ruby) are not encoded into ISO 8601 as required by the QBO API. This causes a QboApi::BadRequest error to be raised.

qbo_api.create(:time_activity, payload: 
    {
        NameOf: 'Employee', 
        EmployeeRef: {value: 1}, 
        StartTime: Time.parse('2018-01-24T09:10:18+00:00'), 
        EndTime: Time.parse('2018-01-24T09:12:18+00:00')
    }
)

QboApi::BadRequest: [{
    :fault_type=>"ValidationFault", 
    :error_code=>"2010", 
    :error_message=>"Request has invalid or unsupported property", 
    :error_detail=>"Property Name:2018-01-24 09:10:18 UTC (through reference chain: com.intuit.schema.finance.v3.TimeActivity[\"StartTime\"]) specified is unsupported or invalid"}]

I noticed that ActiveSupport already does this by patching the #to_json method for the relevant classes. I was thinking we could fix this without patching the core by working with a #dup of the payload hash, scanning the values for the relevant classes and changing them to ISO 8601 strings before sending the dup'ed payload through JSON.generate. That would only support datetimes in the payload root, though-- otherwise we would need something like ActiveSupport's #deep_dup before doing a recursive replacement. I'd need to double-check if there are any entities with nested datetimes.

Thoughts on implementing?

Create methods not working

I can successfully fetch data from the API.
The following work as expected

I have made a custom class to store HashKey

qboapi = QboApi.new( access_token: HashKey.fetch( 'QUICKBOOKS_ACCESS' ), realm_id: HashKey.fetch( 'QUICKBOOKS_REALM_ID' ) )

qboapi.get :customer, 1
order = Order.last

Having problems creating entries.

The same payload works if I try it on PostMan

data = '{
"BillAddr": {
"Line1": "123 Main Street",
"City": "Mountain View",
"Country": "USA",
"CountrySubDivisionCode": "CA",
"PostalCode": "94042"
},
"Notes": "Here are other details.",
"DisplayName": "King Groceries1",
"PrimaryPhone": {
"FreeFormNumber": "(555) 555-5555"
},
"PrimaryEmailAddr": {
"Address": "[email protected]"
}
}'

qboapi.create(:customer, payload: data)

The is the error

[{:fault_type=>"ValidationFault", :error_code=>"500", :error_message=>"Unsupported Operation", :error_detail=>"Operation Could not find resource for relative : /v3/company/193514733709249/customer of full path: https://sandbox.qbo.intuit.com/qbo51/v3/company/193514733709249/customer is not supported."}]

Why would a query request intermittently produce an error with "error_code: 120"

Wondering if you might now what this error message means.

It intermittently occurs when I running tests in RSpec which run the query ("Select * from Accounts") with the api. Same credentials are being used on multiple tests in the same suite, usually the tests work fine, but with some regularity I get the error below, and I can't figure out the cause,
Once in a while I see the error in prod which is particularly concerning.

My first thought was that the test suite was hitting the api much and this was some kind of throttling problem. But there are specific error codes for throttling and this isn't one of them. The only reference I could find to an "error_code: 120" says something about

'AuthorizationFailure : XXX Accountant user was deleted while still connected to the company.'

But I'm not deleting any 'account user' at all. Not 100% sure what an accountant user actually is.

Any thoughts, how I can stop it?

 QboApi::InternalServerError:
       [{:fault_type=>"AuthorizatonFault", :error_code=>"120", :error_message=>"Authorization Failure", :error_detail=>"AuthorizationFailure: Unknown Error during Authentication, statusCode: 500"}]


Minor version as instance variable

I ran into a scenario recently where I needed to use a lesser minor version for specific requests. However, the current implementation was not thread-safe to be changing QboApi.minor_version between requests. I decided to add an instance variable for #minor_version, as you can see on my fork.

Would you like me to submit a PR?

Updates are not passing Id correctly

set_update sets the Id in the outgoing request as resp['Id'], but resp is coming back with a hash with the entity name as the key and a hash as the value.

{"SalesReceipt"=>{"domain"=>"QBO", "sparse"=>false, "Id"=>"15829", ...}}

It seems like you intend for those responses to be one level down. Or am I missing something?

Thanks!

SKU's not showing up

Steps to reproduce

  1. create an item with product code/SKU using the Intuit web interface
  2. use the Intuit API explorer to view the item. The SKU code is present
  3. use qbo_api gem to get the same item. The SKU code is not present

Is this due to minor version issues? How should I set the minor version parameter in my calls to Intuit?

Quickbooks API is down now. Great time to test out your 50x handling :)

QboApi::InternalServerError: [{:fault_type=>"SERVICE", :error_code=>"3100", :error_message=>"message=InternalServerError; errorCode=003100; statusCode=500", :error_detail=>nil}]

QboApi::BadRequest: [{:fault_type=>"ValidationFault", :error_code=>"500", :error_message=>"Unsupported Operation", :error_detail=>"Operation Could not find resource for relative : /v3/company/:realmid/vendor of full path: https://c10.qbo.intuit.com/qbo10/v3/company/:realmid/vendor?minorversion=12&requestid=2-d5b1c12d1a7ab42ef1de8d79 is not supported."}]

Faraday::ClientError: the server responded with status 502

{:response=>{:data=>":nil_data", :status=>:no_response}, :client_error=>"#<Faraday::ClientError response={:status=>503, :headers=>{\"content-type\"=>\"text/html\", \"content-length\"=>\"213\", \"connection\"=>\"close\", \"server\"=>\"nginx\", \"date\"=>\"Tue, 03 Apr 2018 20:34:21 GMT\", \"strict-transport-security\"=>\"max-age=15552000\", \"intuit_tid\"=>\"8cb2e5b6-76a4-d4b0-a223-02300df8004b\"}, :body=>\"<html>\\r\\n<head><title>503 Service Temporarily Unavailable</title></head>\\r\\n<body bgcolor=\\\"white\\\">\\r\\n<center><h1>503 Service Temporarily Unavailable</h1></center>\\r\\n<hr><center>Avi Vantage/</center>\\r\\n</body>\\r\\n</html>\\r\\n\"}>"}
--> apparently they use https://github.com/avinetworks https://avinetworks.com/docs/

=> #<Faraday::ClientError response={:status=>503, :headers=>{"content-type"=>"application/json", "content-length"=>"0", "connection"=>"close", "server"=>"nginx", "date"=>"Tue, 03 Apr 2018 21:54:06 GMT", "intuit_tid"=>"d370a1fa-26b3-2c90-7bd5-aa47c3fbd61f"}, :body=>""}>

TypeError: superclass mismatch for class Cipher

Going through your setup instructions I get the following error:

Boot Error
Something went wrong while loading example/oauth2.rb

TypeError: superclass mismatch for class Cipher
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/2.4.0/openssl/cipher.rb:64:in `<class:Cipher>'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/2.4.0/openssl/cipher.rb:16:in `<module:OpenSSL>'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/2.4.0/openssl/cipher.rb:15:in `<top (required)>'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/2.4.0/openssl.rb:17:in `require'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/2.4.0/openssl.rb:17:in `<top (required)>'
/Users/z/.rvm/gems/ruby-2.4.2/gems/rack-2.0.7/lib/rack/session/cookie.rb:1:in `require'
/Users/z/.rvm/gems/ruby-2.4.2/gems/rack-2.0.7/lib/rack/session/cookie.rb:1:in `<top (required)>'
/Users/z/.rvm/gems/ruby-2.4.2/gems/sinatra-2.0.5/lib/sinatra/base.rb:1776:in `require'
/Users/z/.rvm/gems/ruby-2.4.2/gems/sinatra-2.0.5/lib/sinatra/base.rb:1776:in `<class:Base>'
/Users/z/.rvm/gems/ruby-2.4.2/gems/sinatra-2.0.5/lib/sinatra/base.rb:894:in `<module:Sinatra>'
/Users/z/.rvm/gems/ruby-2.4.2/gems/sinatra-2.0.5/lib/sinatra/base.rb:22:in `<top (required)>'
/Users/z/.rvm/gems/ruby-2.4.2/gems/sinatra-2.0.5/lib/sinatra/main.rb:1:in `require'
/Users/z/.rvm/gems/ruby-2.4.2/gems/sinatra-2.0.5/lib/sinatra/main.rb:1:in `<top (required)>'
/Users/z/.rvm/gems/ruby-2.4.2/gems/sinatra-2.0.5/lib/sinatra.rb:1:in `require'
/Users/z/.rvm/gems/ruby-2.4.2/gems/sinatra-2.0.5/lib/sinatra.rb:1:in `<top (required)>'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/site_ruby/2.4.0/bundler/runtime.rb:81:in `require'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/site_ruby/2.4.0/bundler/runtime.rb:81:in `block (2 levels) in require'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/site_ruby/2.4.0/bundler/runtime.rb:76:in `each'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/site_ruby/2.4.0/bundler/runtime.rb:76:in `block in require'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/site_ruby/2.4.0/bundler/runtime.rb:65:in `each'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/site_ruby/2.4.0/bundler/runtime.rb:65:in `require'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/site_ruby/2.4.0/bundler/inline.rb:70:in `gemfile'
/Users/z/git/lp/qbo_api/example/oauth2.rb:6:in `<top (required)>'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/site_ruby/2.4.0/rubygems/core_ext/kernel_require.rb:54:in `require'
/Users/z/.rvm/rubies/ruby-2.4.2/lib/ruby/site_ruby/2.4.0/rubygems/core_ext/kernel_require.rb:54:in `require'
/Users/z/.rvm/gems/ruby-2.4.2/gems/shotgun-0.9.2/lib/shotgun/loader.rb:115:in `inner_app'
/Users/z/.rvm/gems/ruby-2.4.2/gems/shotgun-0.9.2/lib/shotgun/loader.rb:103:in `assemble_app'
/Users/z/.rvm/gems/ruby-2.4.2/gems/shotgun-0.9.2/lib/shotgun/loader.rb:86:in `proceed_as_child'
/Users/z/.rvm/gems/ruby-2.4.2/gems/shotgun-0.9.2/lib/shotgun/loader.rb:31:in `call!'
/Users/z/.rvm/gems/ruby-2.4.2/gems/shotgun-0.9.2/lib/shotgun/loader.rb:18:in `call'
/Users/z/.rvm/gems/ruby-2.4.2/gems/shotgun-0.9.2/lib/shotgun/favicon.rb:12:in `call'
/Users/z/.rvm/gems/ruby-2.4.2/gems/rack-2.0.7/lib/rack/urlmap.rb:68:in `block in call'
/Users/z/.rvm/gems/ruby-2.4.2/gems/rack-2.0.7/lib/rack/urlmap.rb:53:in `each'
/Users/z/.rvm/gems/ruby-2.4.2/gems/rack-2.0.7/lib/rack/urlmap.rb:53:in `call'
/Users/z/.rvm/gems/ruby-2.4.2/gems/rack-2.0.7/lib/rack/builder.rb:153:in `call'
/Users/z/.rvm/gems/ruby-2.4.2/gems/puma-3.12.0/lib/puma/configuration.rb:225:in `call'
/Users/z/.rvm/gems/ruby-2.4.2/gems/puma-3.12.0/lib/puma/server.rb:658:in `handle_request'
/Users/z/.rvm/gems/ruby-2.4.2/gems/puma-3.12.0/lib/puma/server.rb:472:in `process_client'
/Users/z/.rvm/gems/ruby-2.4.2/gems/puma-3.12.0/lib/puma/server.rb:332:in `block in run'
/Users/z/.rvm/gems/ruby-2.4.2/gems/puma-3.12.0/lib/puma/thread_pool.rb:133:in `block in spawn_thread'
$ ruby --version
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin18]

seen this before?

Issue with Vendor Credits and Bill Payments?

Hello,

While creating a bill payment, with the following body:

{"VendorRef"=>{"value"=>"238", "name"=>"Vendor"}, "DocNumber"=>"11761", "TxnDate"=>"2018-01-21", "TotalAmt"=>10.95, "PrivateNote"=>"For Purchase Order: XYZABC", "Line"=>[{"Amount"=>10.95, "LinkedTxn"=>[{"TxnId"=>3385, "TxnType"=>"Bill"}]}], "CurrencyRef"=>{"value"=>"USD"}, "ExchangeRate"=>5.66, "PayType"=>"CreditCard", "CreditCardPayment"=>{"CCAccountRef"=>{"value"=>"771", "name"=>"American Express - 5555"}}}

The payment gets created succesfully, but I get the following Debug Warning:

DEBUG -- : [QuickBooks] entity name not in response body: entity=:billpayment entity_name="Billpayment" body={..

When I then try to update the bill payment, instead of updating the payment, for some reason, a new payment gets created:

@qbo_api.update(:billpayment, id: qbo_payment_id, payload: purchase_transaction_payload)

Both the issues also happen with vendor credits (ie. Debug and duplication on update)

I think, we also need to setup Authorization header in every request

We need to send access_token as Bearer token in authorization header.

   Authorization: "Bearer #{@access_token}"

I am getting following error:

QboApi::Unauthorized: [{:fault_type=>"AUTHENTICATION", :error_code=>"3200", :error_message=>"message=AuthenticationFailed; errorCode=003200; statusCode=401", :error_detail=>""}]

How to update preferences?

I keep getting a 10000 system level error when I try this:

qbo_api.update(:preferences, id: "", payload: preferences)

or a method failure when I omit the id.

Any help updating Preferences as well as any other supporting_entities would be amazing, thanks!

Purchase objects returned without root key?

When I execute a api.get("PurchaseOrder", 167) I get a json result that looks like:

{"PurchaseOrder=>
{"ShipAddr"=>
{"id"="99",
[...] }
}

i.e. everything is under root key "PurchaseOrder"

When I execute a get for a purchase, api.get("Purchase", 157). I get a json result without a root, instead I get a hash that starts as below, with no root key "Purchase"=>, instead just a collection of elements.

{
"AccountRef"=>{"value"=>152, "name"="Some Account},
"PaymentType"=>"Check",
[...]
}

Is that by design? When you run read requests in the API explorer both purchase and purchaseorder return a root key. Wondering why QboApi only does sometime and if there some way to know when it will and when it won't?

client.authorization_uri doesn't encode ampersands properly in Rails 5?

Hi there. I was able to get your example up and running just fine, but when I ported the code into my own rails project I run up against an error trying to authenticate with oauth 2.

The url produced in the connect to quickbooks button produced in the eample app looks like this:

https://appcenter.intuit.com/connect/oauth2client_id=Q0tvAA[...]&response_type=code&scope=com.intuit.quickbooks.accounting&state=bdafa[...]
This works fine and gets me to the authentication page.

The url using the same code as in your example in my rails app looks like this:

https://appcenter.intuit.com/connect/oauth2client_id=Q0tvAA&amp;response_type=code&amp;scope=com.intuit.quickbooks.account

this doesn't work and brings up an error page saying the "The response_type query parameter is missing from the authorization request".

My assumption is because insteadd of coding &response=... in the url it is encoding

& amp;response=

... How to fix this?

Many thanks.

How to avoid spawning another browser session on login?

Hi, in your example app, when you press the quickbooks button it opens a new browser window in chrome, and the redirect url back to my app is called in this now window, rather than on the page where the button is located. Also the new window has the address bar blocked from takiing focus

Is there a way to alter this behavior so that the redirect happens on the page where my button is located? I guess its OK tha thte authentication process spawns a new window (which I could have close) but it is a little confusing if the app continues running in the new browser window on redirect rather than the window where the button was originally pressed.
Thanks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.