Rails and QuickBooks Integration - Part 3

Make sure you read part 1 and part 2 of this series if you haven’t already.

In part 2 I stated that in the article to come:

…we’ll dive into the heart of the integration and discover that both pleasure and pain await us with Action Web Service…

Pleasure and Pain

Action Web Service is pure pain. Action Web Service is your best friend.

Dealing with SOAP and XML-RPC sucks, plain and simple. Yet if you have to deal with these finicky protocols, Action Web Service (AWS) can make your life substantially easier. I say that AWS is pure pain simply because it deals with the aforementioned crappy protocols. Trust me, you’ll be dreaming of REST after sniffing a couple dozen SOAP messages wondering why your web service action is not being called. In fact, I doubt I would have made it through if Kent Sibilev would not have went above and beyond the call of duty in answering a number of questions I posted about AWS on the rails mailing list. Kent, thanks for the help.

The QBWC Callbacks

In the QuickBooks Web Connector (QBWC) Programmers Guide in the SDK all the callback methods we need to implement are described in a section called the “QBWC Callback Web Method Reference”. We summarized theses methods in part 1 where we learned that there are seven methods that we must implement and expose to facilitate QBWC communication.

QBWC callback web methods

  • authenticate
  • clientVersion
  • closeConnection
  • connectionError
  • getLastError
  • receiveResponseXML
  • sendRequestXML

But how can we expose these methods to an external SOAP or XML-RPC client? The answer is Action Web Service. I highly recommend reading the chapter on AWS in Agile Web Development with Rails. It will give you a brief overview of AWS and explain the basics like API definition classes. Read it!

Done reading? Good… I’ll assume that you now understand the basics of AWS.

The API Definition

The first thing that we need do is provide our quickbooks controller with an API definition so it can understand how to route and respond to incoming messages.

app/apis/quickbooks_api.rb

class QuickbooksApi < ActionWebService::API::Base
  inflect_names false

  # --- [ QBWC version control ] ---
  # Expects:
  #   * string strVersion = QBWC version number
  # Returns string: 
  #   * NULL or <emptyString> = QBWC will let the web service update
  #   * "E:<any text>" = popup ERROR dialog with <any text>, abort update and force download of new QBWC.
  #   * "W:<any text>" = popup WARNING dialog with <any text>, and give user choice to update or not.
  api_method :clientVersion, 
             :expects => [{:strVersion => :string}], 
             :returns => [[:string]]
             
  # --- [ Authenticate web connector ] ---
  # Expects: 
  #   * string strUserName = username from QWC file
  #   * string strPassword = password
  # Returns string[2]: 
  #   * string[0] = ticket (guid)
  #   * string[1] =
  #       - empty string = use current company file
  #       - "none" = no further request/no further action required
  #       - "nvu" = not valid user
  #       - any other string value = use this company file             
  api_method :authenticate,
             :expects => [{:strUserName => :string}, {:strPassword => :string}], 
             :returns => [[:string]]

  # --- [ To facilitate capturing of QuickBooks error and notifying it to web services ] ---
  # Expects: 
  #   * string ticket  = A GUID based ticket string to maintain identity of QBWebConnector 
  #   * string hresult = An HRESULT value thrown by QuickBooks when trying to make connection
  #   * string message = An error message corresponding to the HRESULT
  # Returns string:
  #   * "done" = no further action required from QBWebConnector
  #   * any other string value = use this name for company file           
  api_method :connectionError,
             :expects => [{:ticket => :string}, {:hresult => :string}, {:message => :string}],
             :returns => [[:string]]             

  # --- [ Facilitates web service to send request XML to QuickBooks via QBWC ] ---
  # Expects:
  #   * int qbXMLMajorVers
  #   * int qbXMLMinorVers
  #   * string ticket
  #   * string strHCPResponse 
  #   * string strCompanyFileName 
  #   * string Country
  #   * int qbXMLMajorVers
  #   * int qbXMLMinorVers
  # Returns string:
  #   * "any_string" = Request XML for QBWebConnector to process
  #   * "" = No more request XML
  api_method :sendRequestXML, 
             :expects => [{:ticket => :string}, {:strHCPResponse => :string}, 
                          {:strCompanyFileName => :string}, {:Country => :string}, 
                          {:qbXMLMajorVers => :int}, {:qbXMLMinorVers => :int}],
             :returns => [:string]

  # --- [ Facilitates web service to receive response XML from QuickBooks via QBWC ] ---
  # Expects:
  #   * string ticket
  #   * string response
  #   * string hresult
  #   * string message
  # Returns int:
  #   * Greater than zero  = There are more request to send
  #   * 100 = Done. no more request to send
  #   * Less than zero  = Custom Error codes
  api_method :receiveResponseXML, 
             :expects => [{:ticket => :string}, {:response => :string}, 
                          {:hresult => :string}, {:message => :string}],
             :returns => [:int]

  # --- [ Facilitates QBWC to receive last web service error ] ---
  # Expects:
  #   * string ticket
  # Returns string:
  #   * error message describing last web service error
  api_method :getLastError,
             :expects => [{:ticket => :string}],
             :returns => [:string]

  # --- [ QBWC will call this method at the end of a successful update session ] ---
  # Expects:
  #   * string ticket 
  # Returns string:
  #   * closeConnection result. Ex: "OK"
  api_method :closeConnection,
             :expects => [{:ticket => :string}],
             :returns => [:string]

end

The Controller

Now we need to add the callback methods to our quickbooks controller. AWS will delegate to the appropriate action based on the incoming message.

app/controllers/quickbooks_controller.rb

class QuickbooksController < ApplicationController
  ssl_required :api, :qwc
  before_filter :set_soap_header, :except => :qwc

  def clientVersion(version)
  end
  
  def authenticate(username, password)
  end

  def connectionError(ticket, hresult, message)
  end

  def sendRequestXML(ticket, hpc_response, company_file_name, country, qbxml_major_version, qbxml_minor_version)
  end

  def receiveResponseXML(ticket, response, hresult, message)
  end

  def getLastError(ticket)
  end

  def closeConnection(ticket)
  end

  def qwc
    ...
  end

  private  
 
    def set_soap_header
      if request.env['HTTP_SOAPACTION'].blank? || request.env['HTTP_SOAPACTION'] == %Q("")
        xml = REXML::Document.new(request.raw_post)
        element = REXML::XPath.first(xml, '/soap:Envelope/soap:Body/*')
        request.env['HTTP_SOAPACTION'] = element.name if element
      end
    end

end

First we’ve added the seven callback methods to our controller. Secondly, we’ve added a before filter to set a soap header. The SOAP specification states that an HTTP client must set a SOAPAction HTTP header field. AWS uses this header value to delegate routing to the correct controller action. Unfortunately QBWC (version 1.0) sets this value to two double quotes (””). Because of this we must sniff the raw post and set the header so AWS can handle the routing.

AWS GET Patch

When the customer first loads the QWC file into QBWC an HTTP GET request is made to the AppUrl. This is our endpoint which we mounted at apis/quickbooks/api. The problem is that AWS will return a 500 error status code on any GET request. This causes QBWC to complain by popping up an error box. Not good…

Fortunately the solution is a simple monkey patch.

lib/action_web_service_ext.rb

module ActionController
  class Base
    
    alias_method :old_dispatch_web_service_request, :dispatch_web_service_request
    
    # --- [ QBWC requests the api url with a GET request upon loading the QWC file for the first time ] ---
    def dispatch_web_service_request
      render :nothing => true and return if request.get?
      old_dispatch_web_service_request
    end
  
  end
end

Don’t forget to load the patch in your environment file.

config/environment.rb

require 'action_web_service_ext.rb'

Testing

Time to have our first successful communication with QBWC!

The docs state that if authenticate() returns the string ‘none’, QBWC will assume that we have no pending requests and will call closeConnection() and exit. So let’s test that flow by adding a bit of code to the two actions to be invoked.

quickbooks_controller.rb

class QuickbooksController < ApplicationController

  ...

  def authenticate(username, password)
    ['85B41BEE-5CD9-427a-A61B-83964F1EB426', 'none']
  end

  def closeConnection(ticket)
    'OK'
  end

  ...

end

Now deploy, restart those mongrels, and click ‘Update Selected’ on your QBWC. If you run a tail on your production.log you should see two web requests, one to authenticate() and one to closeConnection(). QBWC should show green status bars at 100% complete. If you click the ‘Click for more information’ link, the ‘OK’ message returned from closeConnection() will be shown.

Next steps

Congratulations on your first QWBC <=> hosted app communication!

Some brave souls might return ” from authenticate() to see what lies ahead. All others can just wait for the next installment…

Comment or question via
FYI: This post was migrated over from another blogging engine. If you encounter any issues please let me know on . Thanks.