Rails and QuickBooks integration - Part 3

December 13th, 2006 QuickbooksRails

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

  1. Comment by Micahel on 12/14/06

    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.

  2. Comment by Michael on 12/14/06

    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 :-)

  3. Comment by michael on 12/14/06

    yikes! sorry about that terrible formatting above. I was trying to make a bulleted list.

  4. Comment by Aaron Blohowiak on 12/14/06

    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.

  5. Comment by Zack on 12/14/06

    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.

  6. Comment by Micahel on 12/14/06

    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.