Integrate QuickBooks Desktop with Ruby on Rails

Demo Rails App

Set your QUBE API key and secret as environment variables:

$ export QUBE_API_KEY=your_api_key
$ export QUBE_WEBHOOK_SECRET=your_webhook_secret

Migrations

Store metadata about your customer's QuickBooks Desktop connection. Keeping track of the company file helps prevent accidental data corruption when your customer inevitably changes it.

# db/migrate/20210101000000_create_quickbooks_desktop_connections.rb
class CreateQuickbooksDesktopConnections < ActiveRecord::Migration[6.0]
  def change
    create_table :quickbooks_desktop_connections do |t|
      t.references :account, null: false, foreign_key: true
      t.string :company_file
      t.string :username
    end
  end
end

Track the requests you've queued for the Web Connector, so you can process the responses later.

# db/migrate/20210101000001_create_quickbooks_desktop_requests.rb
class CreateQuickbooksDesktopRequests < ActiveRecord::Migration[6.0]
  def change
    create_table :quickbooks_desktop_requests do |t|
      t.references :quickbooks_desktop_connection, null: false, foreign_key: true
      t.string :qube_request_id, null: false
      t.jsonb :request_json
      t.text :response_json
      t.string :state, null: false, default: 'queued'
    end
  end
end

Create models for the QuickBooks entities you care about

# db/migrate/20210101000001_create_quickbooks_desktop_customers.rb
class CreateQuickbooksDesktopCustomers < ActiveRecord::Migration[6.0]
  def change
    create_table :quickbooks_desktop_customers do |t|
      t.references :quickbooks_desktop_connection, null: false, foreign_key: true
      t.string :list_id, null: false
      t.string :full_name
      t.string :company_name
      t.string :first_name
      t.string :last_name
      t.string :email
      t.string :phone
    end
  end
end

The models

# app/models/account.rb
class Account < ApplicationRecord
  has_one :qbd_connection, class_name: 'QuickbooksDesktop::Connection', dependent: :destroy
end
#app/models/quickbooks_desktop/connection.rb
class QuickbooksDesktop::Connection < ApplicationRecord
  belongs_to :account
  has_many :requests, class_name: 'QuickbooksDesktop::Request', dependent: :destroy
  has_many :customers, class_name: 'QuickbooksDesktop::Customer', dependent: :destroy
end
# app/models/quickbooks_desktop/request.rb
class QuickbooksDesktop::Request < ApplicationRecord
  belongs_to :qbd_connection, class_name: 'QuickbooksDesktop::Connection'

  after_create :queue_request

  def queue_request
    response = QubeSync.queue_request(qbd_connection.connection_id, { request_json: as_qube_json })
    update!(qube_request_id: response.fetch("id"))
  end

  def webhook_url
    Rails.application.routes.url_helpers.webhooks_qube_sync_url
  end
end

# app/models/quickbooks_desktop/customer_query_request.rb
class QuickbooksDesktop::CustomerQueryRequest < QuickbooksDesktop::Request
  def as_qube_json
    QubeSync::Builder.new(version: "15.0") do |b|
      b.QBXML do
        b.QBXMLMsgsRq(onError: "stopOnError", iterator: "Start") do
          b.CustomerQueryRq(requestID: id) do
            b.MaxReturned(25)
          end
        end
      end
    end.as_json
  end

  def process_response!(response)
    case response.fetch('state')
    when 'response_received'
      # this might be called multiple times for a single request
      # if you specified `iterator="Start"` in the request
      json = response.fetch('response_json')
      json.fetch('data').map do |attrs|
        qbd_connection
          .customers
          .create_with(
            full_name: attrs['FullName'],
            company_name: attrs['CompanyName'],
            first_name: attrs['FirstName'],
            last_name: attrs['LastName'],
            email: attrs['Email'],
            phone: attrs['Phone'])
          .find_or_create_by!(list_id: attrs['list_id'])

      if json.fetch('iteratorRemainingCount') > 0
        update!(state: 'iterating')
      else
        update!(state: 'completed')
      end
    when 'error'
      update!(state: 'error',
              error_message: response.dig('error', 'message'),
              error_type: response.dig('error', 'error_type'),
              user_message: response.dig('error', 'user_message'))
    else

    end
  end
end

Customize the Quickbooks entity models to fit your needs. You'll use the response from QuickBooks to store the data in these models.

# app/models/quickbooks_desktop/customer.rb
class QuickbooksDesktop::Customer < ApplicationRecord
  belongs_to :qbd_connection
end

Time to set up the QuickBooks Desktop connection

# app/controllers/accounts_controller.rb
class QuickbooksDesktop::ConnectionsController < ApplicationController
  before_action :set_account
  before_action :set_qbd_connection, only: [:show, :download_qwc, :get_password]

  def create
    connection =
      QubeSync
        .create_connection(name: account.company_name)
        .fetch("id")

    @qbd_connection = @account.create_qbd_connection(connection_id: connection_id)

    initial_sync_requests = [
      QuickbooksDesktop::CustomerQueryRequest.new,
      # QuickbooksDesktop::TaxCodeQueryRequest.new,
      # QuickbooksDesktop::ItemQueryRequest.new,
      # QuickbooksDesktop::VendorQueryRequest.new,
      # ...
    ]
    
    # Queue up the initial requests to QuickBooks Desktop,
    # usually importing data like customers, items, etc.
    @qbd_connection.requests = initial_sync_requests

    redirect_to quickbooks_desktop_connection_path(@qbd_connection)
  end

  def show
    @qbd_connection = @account.qbd_connection

    # render instructions for the user, along with
    # links to download the QWC file and get their
    # Web Connector password (see below)
  end

  def download_qwc
    qwc_content = QubeSync.download_qwc(@qbd_connection.connection_id)
    send_data qwc_content, filename: "qube.qwc"
  end

  def get_password
    password = QubeSync.generate_password(@qbd_connection.connection_id)
    render json: { password: password }
  end

  private

  def set_qbd_connection
    @qbd_connection = @account.qbd_connection
  end
end

Webhook Processing

# app/controllers/webhooks/qube_sync_controller.rb
class Webhooks::QubeSyncController < ApplicationController
  skip_before_action :verify_authenticity_token
  skip_before_action :authenticate_user!

  def customer_query_response
    response = QubeSync.verify_webhook!(
      request.body.read,
      request.headers['X-QUBE-Signature']
    )

    # We background the response processing so we can respond to the webhook quickly
    ProcessCustomerQueryResponseJob.perform_later(response)
    head :ok
  end
end

Response Processing

# app/jobs/process_customer_query_response_job.rb
class ProcessCustomerQueryResponseJob < ApplicationJob
  def perform(response)
    request_id = response.fetch('id')
    
    request = QuickbooksDesktop::Request.find_by(qube_request_id: request_id)
    request.process_response!(response)
  end
end

Request Types

# app/models/quickbooks_desktop/customer_query_request.rb
class QuickbooksDesktop::CustomerQueryRequest < QuickbooksDesktop::Request
  def to_xml
    <<~XML
      <?xml version="1.0" encoding="utf-8"?>
      <?qbxml version="16.0"?>
      <QBXML>
        <QBXMLMsgsRq onError="stopOnError" iterator="Start">
          <CustomerQueryRq requestID="#{id}">
            <MaxReturned>25</MaxReturned>
          </CustomerQueryRq>
        </QBXMLMsgsRq>
      </QBXML>
    XML
  end
end