/intacct-ruby

A Ruby gem wrapper for the Intacct API

Primary LanguageRubyMIT LicenseMIT

Build Status

IntacctRuby

A wrapper for Intacct's API, which tries to stay as close as it can to the syntax and philosophy of the API itself.

The Power of Multi-Function Api Calls

Unlike the other Gems out in the Rubyverse, this library supports one of the Intacct API's most powerful features: multi-function API calls.

Why Does This Matter?

In an ERP system like Intacct, you'll probably want to perform multiple actions at once, like debiting one account and crediting another, or creating several associated records simulatenously. The more calls you make, the longer it will take to see a response. That's just a fact. But if you can bundle all of those actions together into a single call, you lower the load on both your system and Intacct's servers and guarantee yourself a quicker response. Intacct's entire API is built around this idea, and IntacctRuby translates that philosophy into Ruby.

Putting Gem to Use

Let's say you want to create a project and a customer associated with that project simultaneously. The Intacct API would tell you to create a call with a <create><CUSTOMER> function followed by a <create><PROJECT> function. So let's do it!

# REQUEST_OPTS contains authentication information. See 'Authentication' section
# for more information.
request = IntacctRuby::Request.new(REQUEST_OPTS)

request.create object_type: :CUSTOMER, parameters: {
  CUSTOMERID: '1',
  FIRST_NAME: 'Han',
  LAST_NAME: 'Solo',
  TYPE: 'Person',
  EMAIL1: 'han@solo.com',
  STATUS: 'active'
}

request.create object_type: :PROJECT, parameters: {
  PROJECTID: '1',
  NAME: 'Get Chewie a Haircut',
  PROJECTCATEGORY: 'Improve Wookie Hygene',
  CUSTOMERID: '1',
  SHAMPOO: 'true', # a custom field
  BLOWDRY: 'false' # a custom field
}

request.send

Note: Here :CUSTOMER and :PROJECT are object-types which are tagged just after the function tag create and are case-sensitive along with the extra-parameters(CUSTOMERID, FIRST_NAME ..)

This will fire off a request that looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<request>
   <control><!-- Authentication Params --></control>
   <operation transaction="true">
      <authentication><!-- Authentication Params --></authentication>
      <content>
         <function controlid="create-customer-2017-08-03 17:02:40 UTC">
            <create>
               <CUSTOMER>
                  <CUSTOMERID>1</CUSTOMERID>
                  <FIRST_NAME>Han</FIRST_NAME>
                  <LAST_NAME>Solo</LAST_NAME>
                  <TYPE>Person</TYPE>
                  <EMAIL1>han@solo.com</EMAIL1>
                  <STATUS>active</STATUS>
               </CUSTOMER>
            </create>
         </function>
         <function controlid="create-project-2017-08-03 17:02:40 UTC">
            <create>
               <PROJECT>
                  <PROJECTID>1</PROJECTID>
                  <NAME>Get Chewie a Haircut</NAME>
                  <PROJECTCATEGORY>Improve Wookie Hygene</PROJECTCATEGORY>
                  <CUSTOMERID>1</CUSTOMERID>
                  <SHAMPOO>true</SHAMPOO>
                  <BLOWDRY>false</BLOWDRY>
               </PROJECT>
            </create>
         </function>
      </content>
   </operation>
</request>

Legacy Endpoints

The Intacct team has deprecated a number of endpoints which are now only accessible via the legacy API. This API has a different expectation on XML body syntax as well as parameter order enforcement via XSD. The process to construct a legacy request is similar to a standard request, just using the LegacyRequest class.

request = IntacctRuby::LegacyRequest.new(REQUEST_OPTS)

request.create object_type: :record_cctransaction, parameters: {
  chargecardid: '1',
  paymentdate: { year: 2019, month: 11, day: 11 },
  ccpayitems: [
    cpayitem: {
      glaccountno: 1234,
      paymentamount: '0.99',
      locationid: 1,
    }
  ]
}

request.create object_type: :reverse_cctransaction, parameters: {
  key: '4321',
  datereversed: { year: 2019, month: 11, day: 11 },
  memo: 'Purchase returned for refund'
}

request.send

The legacy request class will generate XML that looks something like this.

<?xml version="1.0" encoding="UTF-8"?>
<request>
   <control><!-- Authentication Params --></control>
   <operation transaction="true">
      <authentication><!-- Authentication Params --></authentication>
      <content>
         <function controlid="create-record_cctransaction-2019-06-30 16:45:12 UTC">
            <record_cctransaction>
               <chargecardid>1</chargecardid>
               <paymentdate>
                  <year>2019</year>
                  <month>11</month>
                  <day>11</day>
               </paymentdate>
               <ccpayitems>
                  <ccpayitem>
                     <glaccountno>1234</glaccountno>,
                     <paymentamount>'0.99'</paymentamount>
                     <locationid>1</locationid>
                  </ccpayitem>
               </ccpayitems>
            </record_cctransaction>
         </function>
         <function controlid="create-reverse_cctransaction-2019-06-30 16:45:12 UTC">
            <reverse_cctransaction>
               <key>4321</key>
               <datereversed>
                  <year>2019</year>
                  <month>11</month>
                  <day>11</day>
               </datereversed>
               <memo>Purchase returned for refund</memo>
            </reverse_cctransaction>
         </function>
      </content>
   </operation>
</request>

Read Requests

The read requests follow a slightly different pattern. The object-type is mentioned inside the object tag as seen here Intacct List Journal Entries. Hence, read requests don't accept a object_type: argument directly, the object type is passed through the parameters argument. The following code will read all GLENTRY objects in a specific interval

Note: The gem encodes the queries to a valid XML so that you don't have to. You can query using the &, >, < operators as seen below.

request = IntacctRuby::Request.new(REQUEST_OPTS)

# Object-Type GLENTRY is sent through the parameters arguments
request.readByQuery parameters: {
  object: 'GLENTRY',
  query: "BATCH_DATE >= '03-01-2018' AND BATCH_DATE <= '03-15-2018'",
  fields: '*',
  pagesize: 100
}

request.send

This will fire off a request that looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<request>
   <control><!-- Authentication Params --></control>
   <operation transaction="true">
      <authentication><!-- Authentication Params --></authentication>
      <content>
         <function controlid="readByQuery-2017-08-03 17:02:40 UTC">
            <readByQuery>
                <object>GLENTRY</object>
                <fields>*</fields>
                <query>BATCH_DATE &gt;= '03-01-2018' AND BATCH_DATE &lt;= '03-15-2018'</query>
                <pagesize>100</pagesize>
            </readByQuery>
         </function>
      </content>
   </operation>
</request>

Similarly, for pagination use the readMore function as mentioned here Intacct Paginate Results

request = IntacctRuby::Request.new(REQUEST_OPTS)

request.readMore parameters: {
  resultId: '7765623332WU1hh8CoA4QAAHxI9i8AAAAA5'
}

request.send

This will fire off a request that looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<request>
   <control><!-- Authentication Params --></control>
   <operation transaction="true">
      <authentication><!-- Authentication Params --></authentication>
      <content>
         <function controlid="readMore-2017-08-03 17:02:40 UTC">
            <readMore>
              <resultId>7765623332WU1hh8CoA4QAAHxI9i8AAAAA5</resultId>
            </readMore>
         </function>
      </content>
   </operation>
</request>

In addition to these functions, you may also call the query function. Intacct released this as a newer version of the readByQuery function. The query function provides for more specific filtering as well as some additional options such as "offset" and "orderby". Advantages of query.

request = IntacctRuby::Request.new(REQUEST_OPTS)

request.query parameters: {
   object: 'VENDOR',
   select: [
     { field: "RECORDNO" },
     { field: "NAME" },
   ],
   filter: {
      like: {
         field: "NAME",
         value: "A%"
      }
   },
   pagesize: 100,
   offset: 50,
   orderby: {
      order: {
         field: "RECORDNO",
         descending: "true"
      }
   }
}

request.send

This will fire off a request that looks something like this:

<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<request>
  <control><!-- Authentication Params --></control>
  <operation transaction=\"true\">
    <authentication><!-- Authentication Params --></authentication>
    <content>
      <function controlid=\"query--202	1-07-26 19:27:38 UTC\">
        <query>
          <object>VENDOR</object>
          <select>
            <field>RECORDNO</field>
          </select>
          <filter>
            <like>
              <field>NAME</field>
              <value>A%</value>
            </like>
          </filter>
          <pagesize>100</pagesize>
          <offset>50</offset>
          <orderby>
            <order>
              <field>RECORDNO</field>
              <descending>true</descending>
            </order>
          </orderby>
        </query>
      </function>
    </content>
  </operation>
</request>

If there are function errors (e.g. you omitted a required field) you'll see an error on response. Same if you see an internal server error, or any error outside of the 2xx range.

Authentication

Before we go any further, make sure you've read the Intacct API Quickstart Guide and their article on constructing XML Requests

In IntacctRuby - as with the Intacct API that the gem wraps - your system credentials are pass along with each separate Request instance. The functions that define a request are followed by a hash that spells out each piece of information required by Intacct for authentication. These fields are:

  • senderid
  • sender_password*
  • userid
  • companyid
  • user_password*

* In Intacct's documentation, these are referred to only as password. This won't work in Rubyland, though, because we can't have multiple hash entries with the same key.

Authentication Example:

IntacctRuby::Request.new(
  some_function,
  another_function,
  senderid: 'some_senderid_value',
  sender_password: 'some_sender_password_value',
  userid: 'some_userid_value',
  companyid: 'some_companyid_value',
  user_password: 'some_user_password_value'
)

Though, it probably makes more sense to keep all of these in some handy constant for easy reuse:

REQUEST_OPTS = {
  senderid: 'some_senderid_value',
  sender_password: 'some_sender_password_value',
  userid: 'some_userid_value',
  companyid: 'some_companyid_value',
  user_password: 'some_user_password_value'
}.freeze

IntacctRuby::Request.new(REQUEST_OPTS)

Authentication with SessionId

Here's an example on how to obtain a session id value and use it in the request

request = IntacctRuby::Request.new(REQUEST_OPTS) # REQUEST_OPTS is from the code example above
request.getAPISession(parameters: {})
response = request.send
hash_response = Nori.new.parse(response.response_body.to_xml)
session_id = hash_response.dig('response', 'operation', 'result', 'data', 'api', 'sessionid')
request_opts_with_session_id = {
   sessionid: session_id,
   senderid: 'some_senderid_value',
   sender_password: 'some_sender_password_value',
}
new_request = IntacctRuby::Request.new(request_opts_with_session_id)
# ...

Important Notes on Authentication

These Are Required!

Obviously, Intacct won't do anything if you don't tell it who you are. To save you the bandwidth, this gem will throw errors if any of these auth params are not provided.

BE SAFE!

Though the examples above show hard-coded username/password pairs, this is a really bad idea to do in production code. Instead, we recommend storing these variables in ENVs, using a tool like Figaro to bring it all together.

Customizing Calls

This gem creates calls using the following defaults:

  • uniqueid: false,
  • dtdversion: 3.0,
  • includewhitespace: false,
  • transaction: true

If you'd like to override any of these, you can do so when you create a new request by adding additional fields to the options hash passed into Request#new:

REQUEST_OPTS = {
  senderid: 'some_senderid_value',
  sender_password: 'some_sender_password_value',
  userid: 'some_userid_value',
  companyid: 'some_companyid_value',
  user_password: 'some_user_password_value'
}

REQUEST_OPTS.merge!(
  uniqueid: 'some_uniqueid_override',
  dtdversion: 'some_dtd_override'
)

IntacctRuby::Request.new(REQUEST_OPTS)

Installation

The Gem Itself

Add this line to your application's Gemfile:

gem 'intacct_ruby'

And then execute:

$ bundle

Or install it yourself as:

$ gem install intacct_ruby

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/privateprep/intacct-ruby/.

This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.