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...


6 Comments
Wow, you certainly don't dissapoint with timing. I think you might want to mention something aboout needing to have ssl and a real domain name set up to use quickbooks web connector. If you are on windows you can get the 1.5 beta of the QBWconnector, but you have to email in and ask for it first. http://idnforums.intuit.com/messageview.aspx?catid=56&threadid=6704&highlight_key=y&keyword1=beta
http://idnforums.intuit.com/messageview.aspx?catid=52&threadid=6806&enterthread=y
I spend a bit of the day getting bind and ssl setup since I am working on a macbook and parrallels enviornment and can't use localhost in my parrallels image.
I got thru all the ssl/dns trickery, but now I am stumped. When trying to add the application in the qwc file, I run into:
<quote> The underlying connection was closed: Could not establish trust relationship with remote server.
TargetSite (method that threw the exception): Void CheckFinalStatus()</quote>
I can get all this things to work if i invoke them directly with webservicescaffold :invoke
Thanks again for writing all this up.
Yeah!! I finally got it to work. It was the ssl stuff that was causing the failure. Just having ssl isn't good enough. You also need a signed certificate, with matching servername and domain name. The regular old localhost self signed certs arn't good enough. so, to summarize in case anybody else is on a mac, with quickbooks running in parallels:
must have ssl enabled for your app url. I did this by turning on mod_proxy in the apache config. directions from here were very helpful.
you must have DNS name resolution. IP address is not acceptable. I set up bind on my router, but eventually would like to set it up locally. Just setting the hosts file might be good enough?
basicly, you have to be able to get to your https app url without getting any warnings about ssl.
thanks again. looking forward to part 4 :-)
yikes! sorry about that terrible formatting above. I was trying to make a bulleted list.
Thank you for your well-written, explicit and thorough contribution to the rails community! You are saving me hours, at least. Eager for part 4.
Michael,
Good point on having a real domain name and a signed certificate by a trusted authority. Right now QBWC is super picky.
Apparently this will change a bit in version 1.5 which should come out within the month. It promises to be able to handle localhost requests without ssl.
Zack, actually I am using 1.5, one of the links in the first comment show how to get a copy. Unfortunatley this will only help if you are serving your rails app from the windows machine. In my case, and i'm guesing some others, my rails app is served from OSX on my laptop, and Quickbooks is running on a Parallels virtual machine, so localhost won't solve the DNS and SSL issues.
However, I was thinking there might be a simple way around this (untested as of yet):
From the windows machine ssh port forward to your mac. for example,
ssh -L80:192.168.1.22:80 192.168.1.22
that would be be entered from the windows install. This would require 1.5 (which Harvey at Quickbooks sent me within a few hours of requesting it.)
Commenting is closed for this article.