class Quickbooks::Service::BaseService

Constants

BASE_DOMAIN
HTTP_ACCEPT
HTTP_ACCEPT_ENCODING
HTTP_CONTENT_TYPE
SANDBOX_DOMAIN
XML_NS

Attributes

base_uri[R]
company_id[RW]
last_response_body[R]
last_response_xml[R]
oauth[RW]

Public Class Methods

new(attributes = {}) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 20
def initialize(attributes = {})
  domain = Quickbooks.sandbox_mode ? SANDBOX_DOMAIN : BASE_DOMAIN
  @base_uri = "https://#{domain}/v3/company"
  attributes.each {|key, value| public_send("#{key}=", value) }
end

Public Instance Methods

access_token=(token) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 26
def access_token=(token)
  @oauth = token
  rebuild_connection!
end
company_id=(company_id) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 31
def company_id=(company_id)
  @company_id = company_id
end
default_model_query() click to toggle source
# File lib/quickbooks/service/base_service.rb, line 78
def default_model_query
  "SELECT * FROM #{self.class.name.split("::").last}"
end
is_json?() click to toggle source
# File lib/quickbooks/service/base_service.rb, line 70
def is_json?
  self.class::HTTP_CONTENT_TYPE == "application/json"
end
is_pdf?() click to toggle source
# File lib/quickbooks/service/base_service.rb, line 74
def is_pdf?
  self.class::HTTP_CONTENT_TYPE == "application/pdf"
end
oauth_v1?() click to toggle source
# File lib/quickbooks/service/base_service.rb, line 40
def oauth_v1?
  @oauth.is_a? OAuth::AccessToken
end
oauth_v2?() click to toggle source
# File lib/quickbooks/service/base_service.rb, line 44
def oauth_v2?
  @oauth.is_a? OAuth2::AccessToken
end
realm_id=(company_id) click to toggle source

realm & company are synonymous

# File lib/quickbooks/service/base_service.rb, line 36
def realm_id=(company_id)
  @company_id = company_id
end
rebuild_connection!() click to toggle source
OAuth2

The default Faraday connection does not have gzip or multipart support.

We need to reset the existing connection and build a new one.

# File lib/quickbooks/service/base_service.rb, line 50
def rebuild_connection!
  return unless oauth_v2?
  @oauth.client.connection = nil
  @oauth.client.connection.build do |builder|
    builder.use :gzip
    builder.request :multipart
    builder.request :url_encoded
    builder.adapter :net_http
  end
end
url_for_base() click to toggle source
# File lib/quickbooks/service/base_service.rb, line 65
def url_for_base
  raise MissingRealmError.new unless @company_id
  "#{@base_uri}/#{@company_id}"
end
url_for_query(query = nil, start_position = 1, max_results = 20, options = {}) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 82
def url_for_query(query = nil, start_position = 1, max_results = 20, options = {})
  query ||= default_model_query
  query = "#{query} STARTPOSITION #{start_position} MAXRESULTS #{max_results}"

  "#{url_for_base}/query?query=#{CGI.escape(query)}"
end
url_for_resource(resource) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 61
def url_for_resource(resource)
  "#{url_for_base}/#{resource}"
end

Private Instance Methods

add_query_string_to_url(url, params) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 301
def add_query_string_to_url(url, params)
  if params.is_a?(Hash) && !params.empty?
    url + "?" + params.collect { |k| "#{k.first}=#{k.last}" }.join("&")
  else
    url
  end
end
check_response(response, options = {}) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 309
def check_response(response, options = {})
  log "------ QUICKBOOKS-RUBY RESPONSE ------"
  log "RESPONSE CODE = #{response.code}"
  if response.respond_to?(:headers)
    log "RESPONSE HEADERS = #{response.headers}"
  end
  log_response_body(response)
  status = response.code.to_i
  case status
  when 200
    # even HTTP 200 can contain an error, so we always have to peek for an Error
    if response_is_error?
      parse_and_raise_exception(options)
    else
      response
    end
  when 302
    raise "Unhandled HTTP Redirect"
  when 401
    raise Quickbooks::AuthorizationFailure
  when 403
    message = parse_intuit_error[:message]
    if message.include?('ThrottleExceeded')
      raise Quickbooks::ThrottleExceeded, message
    end
    raise Quickbooks::Forbidden, message
  when 404
    raise Quickbooks::NotFound
  when 413
    raise Quickbooks::RequestTooLarge
  when 400, 500
    parse_and_raise_exception(options)
  when 429
    message = parse_intuit_error[:message]
    raise Quickbooks::TooManyRequests, message
  when 503, 504
    raise Quickbooks::ServiceUnavailable
  else
    raise "HTTP Error Code: #{status}, Msg: #{response.plain_body}"
  end
end
do_http(method, url, body, headers) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 235
def do_http(method, url, body, headers) # throws IntuitRequestException
  if @oauth.nil?
    raise "OAuth client has not been initialized. Initialize with setter access_token="
  end
  unless headers.has_key?('Content-Type')
    headers['Content-Type'] = self.class::HTTP_CONTENT_TYPE
  end
  unless headers.has_key?('Accept')
    headers['Accept'] = self.class::HTTP_ACCEPT
  end
  unless headers.has_key?('Accept-Encoding')
    headers['Accept-Encoding'] = HTTP_ACCEPT_ENCODING
  end

  log "------ QUICKBOOKS-RUBY REQUEST ------"
  log "METHOD = #{method}"
  log "RESOURCE = #{url}"
  log_request_body(body)
  log "REQUEST HEADERS = #{headers.inspect}"

  raw_response = case method
  when :get
    oauth_get(url, headers)
  when :post
    oauth_post(url, body, headers)
  when :upload
    oauth_post_with_multipart(url, body, headers)
  else
    raise "Do not know how to perform that HTTP operation"
  end
  response = Quickbooks::Service::Responses::OAuthHttpResponse.wrap(raw_response)
  check_response(response, :request => body)
end
do_http_file_upload(uploadIO, url, metadata = nil) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 218
def do_http_file_upload(uploadIO, url, metadata = nil)
  headers = {
    'Content-Type' => 'multipart/form-data'
  }
  body = {}
  body['file_content_0'] = uploadIO

  if metadata
    standalone_prefix = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
    meta_data_xml = "#{standalone_prefix}\n#{metadata.to_xml_ns.to_s}"
    param_part = UploadIO.new(StringIO.new(meta_data_xml), "application/xml")
    body['file_metadata_0'] = param_part
  end

  do_http(:upload, url, body, headers)
end
do_http_get(url, params = {}, headers = {}) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 198
def do_http_get(url, params = {}, headers = {}) # throws IntuitRequestException
  url = add_query_string_to_url(url, params)
  do_http(:get, url, {}, headers)
end
do_http_post(url, body = "", params = {}, headers = {}) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 193
def do_http_post(url, body = "", params = {}, headers = {}) # throws IntuitRequestException
  url = add_query_string_to_url(url, params)
  do_http(:post, url, body, headers)
end
do_http_raw_get(url, params = {}, headers = {}) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 203
def do_http_raw_get(url, params = {}, headers = {})
  url = add_query_string_to_url(url, params)
  unless headers.has_key?('Content-Type')
    headers['Content-Type'] = self.class::HTTP_CONTENT_TYPE
  end
  unless headers.has_key?('Accept')
    headers['Accept'] = self.class::HTTP_ACCEPT
  end
  unless headers.has_key?('Accept-Encoding')
    headers['Accept-Encoding'] = HTTP_ACCEPT_ENCODING
  end
  raw_response = oauth_get(url, headers)
  Quickbooks::Service::Responses::OAuthHttpResponse.wrap(raw_response)
end
fetch_collection(query, model, options = {}) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 112
def fetch_collection(query, model, options = {})
  page = options.fetch(:page, 1)
  per_page = options.fetch(:per_page, 20)

  start_position = ((page - 1) * per_page) + 1 # page=2, per_page=10 then we want to start at 11
  max_results = per_page

  response = do_http_get(url_for_query(query, start_position, max_results))

  parse_collection(response, model)
end
fetch_object(model, url, params = {}) click to toggle source

A single object response is the same as a collection response except it just has a single main element

# File lib/quickbooks/service/base_service.rb, line 101
def fetch_object(model, url, params = {})
  raise ArgumentError, "missing model to instantiate" if model.nil?
  response = do_http_get(url, params)
  collection = parse_collection(response, model)
  if collection.is_a?(Quickbooks::Collection)
    collection.entries.first
  else
    nil
  end
end
log_request_body(body) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 364
def log_request_body(body)
  log "REQUEST BODY:"
  if is_json?
    log(body.inspect)
  elsif is_pdf?
    log("BODY is a PDF : not dumping")
  else
    #multipart request for uploads arrive here in a Hash with UploadIO vals
    if body.is_a?(Hash)
      body.each do |k,v|
        log('BODY PART:')
        val_content = v.inspect
        if v.is_a?(UploadIO)
          if v.content_type == 'application/xml'
            if v.io.is_a?(StringIO)
              val_content = log_xml(v.io.string)
            end
          end
        end
        log("#{k}: #{val_content}")
      end
    else
      log(log_xml(body))
    end
  end
end
log_response_body(response) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 351
def log_response_body(response)
  log "RESPONSE BODY:"
  if is_json?
    log ">>>>#{response.plain_body.inspect}"
    parse_json(response.plain_body)
  elsif is_pdf?
    log("BODY is a PDF : not dumping")
  else
    log(log_xml(response.plain_body))
    parse_xml(response.plain_body)
  end
end
oauth_get(url, headers) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 269
def oauth_get(url, headers)
  if oauth_v1?
    @oauth.get(url, headers)
  elsif oauth_v2?
    @oauth.get(url, headers: headers, raise_errors: false)
  else
    raise InvalidOauthAccessTokenObject.new(@oauth)
  end
end
oauth_post(url, body, headers) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 279
def oauth_post(url, body, headers)
  if oauth_v1?
    @oauth.post(url, body, headers)
  elsif oauth_v2?
    @oauth.post(url, headers: headers, body: body, raise_errors: false)
  else
    raise InvalidOauthAccessTokenObject.new(@oauth)
  end
end
oauth_post_with_multipart(url, body, headers) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 289
def oauth_post_with_multipart(url, body, headers)
  raw_response = if oauth_v1?
                   oauth.post_with_multipart(url, body, headers)
                 elsif oauth_v2?
                   oauth.post_with_multipart(url, headers: headers, body: body, raise_errors: false)
                 else
                   raise InvalidOauthAccessTokenObject.new(@oauth)
                 end
  response = Quickbooks::Service::Responses::OAuthHttpResponse.wrap(raw_response)
  check_response(response, :request => body)
end
parse_and_raise_exception(options = {}) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 391
def parse_and_raise_exception(options = {})
  err = parse_intuit_error
  ex = Quickbooks::IntuitRequestException.new("#{err[:message]}:\n\t#{err[:detail]}")
  ex.code = err[:code]
  ex.detail = err[:detail]
  ex.type = err[:type]
  if is_json?
    ex.request_json = options[:request]
  else
    ex.request_xml = options[:request]
  end
  raise ex
end
parse_collection(response, model) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 124
def parse_collection(response, model)
  if response
    collection = Quickbooks::Collection.new
    xml = @last_response_xml
    begin
      results = []

      query_response = xml.xpath("//xmlns:IntuitResponse/xmlns:QueryResponse")[0]
      if query_response

        start_pos_attr = query_response.attributes['startPosition']
        if start_pos_attr
          collection.start_position = start_pos_attr.value.to_i
        end

        max_results_attr = query_response.attributes['maxResults']
        if max_results_attr
          collection.max_results = max_results_attr.value.to_i
        end

        total_count_attr = query_response.attributes['totalCount']
        if total_count_attr
          collection.total_count = total_count_attr.value.to_i
        end
      end

      path_to_nodes = "//xmlns:IntuitResponse//xmlns:#{model::XML_NODE}"
      collection.count = xml.xpath(path_to_nodes).count
      if collection.count > 0
        xml.xpath(path_to_nodes).each do |xa|
          results << model.from_xml(xa)
        end
      end

      collection.entries = results
    rescue => ex
      raise Quickbooks::IntuitRequestException.new("Error parsing XML: #{ex.message}")
    end
    collection
  else
    nil
  end
end
parse_intuit_error() click to toggle source
# File lib/quickbooks/service/base_service.rb, line 415
def parse_intuit_error
  error = {:message => "", :detail => "", :type => nil, :code => 0}
  fault = @last_response_xml.xpath("//xmlns:IntuitResponse/xmlns:Fault")[0]
  if fault
    error[:type] = fault.attributes['type'].value

    error_element = fault.xpath("//xmlns:Error")[0]
    if error_element
      code_attr = error_element.attributes['code']
      if code_attr
        error[:code] = code_attr.value
      end
      element_attr = error_element.attributes['element']
      if code_attr
        error[:element] = code_attr.value
      end
      error[:message] = error_element.xpath("//xmlns:Message").text
      error[:detail] = error_element.xpath("//xmlns:Detail").text
    end
  end

  error
rescue Nokogiri::XML::XPath::SyntaxError => exception
  error[:detail] = @last_response_xml.to_s

  error
end
parse_singular_entity_response(model, xml, node_xpath_prefix = nil) click to toggle source

Given an IntuitResponse which is expected to wrap a single Entity node, e.g. <IntuitResponse xmlns=“schema.intuit.com/finance/v3” time=“2013-11-16T10:26:42.762-08:00”>

<Customer domain="QBO" sparse="false">
  <Id>1</Id>
  ...
</Customer>

</IntuitResponse>

# File lib/quickbooks/service/base_service.rb, line 176
def parse_singular_entity_response(model, xml, node_xpath_prefix = nil)
  xmldoc = Nokogiri(xml)
  prefix = node_xpath_prefix || model::XML_NODE
  xmldoc.xpath("//xmlns:IntuitResponse/xmlns:#{prefix}")[0]
end
parse_singular_entity_response_for_delete(model, xml) click to toggle source

A successful delete request returns a XML packet like: <IntuitResponse xmlns=“schema.intuit.com/finance/v3” time=“2013-04-23T08:30:33.626-07:00”>

<Payment domain="QBO" status="Deleted">
<Id>8748</Id>
</Payment>

</IntuitResponse>

# File lib/quickbooks/service/base_service.rb, line 188
def parse_singular_entity_response_for_delete(model, xml)
  xmldoc = Nokogiri(xml)
  xmldoc.xpath("//xmlns:IntuitResponse/xmlns:#{model::XML_NODE}[@status='Deleted']").length == 1
end
parse_xml(xml) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 91
def parse_xml(xml)
  @last_response_xml = Nokogiri::XML(xml)
end
response_is_error?() click to toggle source
# File lib/quickbooks/service/base_service.rb, line 405
def response_is_error?
  begin
    @last_response_xml.xpath("//xmlns:IntuitResponse/xmlns:Fault")[0] != nil
  rescue Nokogiri::XML::XPath::SyntaxError => exception
    #puts @last_response_xml.to_xml.to_s
    #puts "WTF: #{exception.inspect}:#{exception.backtrace.join("\n")}"
    true
  end
end
valid_xml_document(xml) click to toggle source
# File lib/quickbooks/service/base_service.rb, line 95
def valid_xml_document(xml)
  %Q{<?xml version="1.0" encoding="utf-8"?>\n#{xml.strip}}
end