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…
