Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
78a2154
setup Intacct service url to allow customize the intacct service url.…
bernespinoza Apr 11, 2023
c9af112
returns response validation in delete payment and invoice
bernespinoza Apr 11, 2023
8550cb5
Updates on_error hook
bernespinoza Apr 19, 2023
da3c49b
Raise errors on error when objects already exists
bernespinoza Apr 19, 2023
183be4c
fixes customer and bill queries because they are failing when retrivi…
bernespinoza Apr 25, 2023
a6d44c6
safety fail on empty response
bernespinoza Apr 25, 2023
765693e
updates intacct system id of vendors
bernespinoza Apr 26, 2023
d6439ea
use object intacct_system_id to get data and update it from intacct
bernespinoza Apr 26, 2023
138ac11
force ach account info numbers to be numbers instead of strings
bernespinoza Apr 27, 2023
5917dc9
customer_fields
bernespinoza Nov 16, 2023
43a1258
fixes for empty values
bernespinoza Nov 24, 2023
6faac5e
remove new files
bernespinoza Nov 24, 2023
9797757
remove intacct_api and response
bernespinoza Nov 24, 2023
372566b
rename traverse to each
bernespinoza Dec 11, 2025
489c6c9
load customer data
bernespinoza Dec 20, 2025
0d05149
create a new invoice object
bernespinoza Dec 20, 2025
d1ecfab
v0.0.3: controlid, timeouts, logger, customer_fields, error nil guard
bernespinoza Mar 14, 2026
f714985
v0.1.0: remove Object monkey-patch, plain Base class, ResourceConfig …
bernespinoza Mar 14, 2026
9cf90e7
logic to read and write
bernespinoza Mar 14, 2026
28eb229
add tests and update readme
bernespinoza Mar 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in intacct.gemspec
gemspec

gem "rails", "~> 4.0.0"
gem 'turnip', github: 'cj/turnip'
gem 'turnip', '>= 3.0'
215 changes: 200 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,214 @@
# Intacct

TODO: Write a gem description
Ruby gem for syncing financial records with the [Sage Intacct XML API](https://developer.intacct.com/web-services/).

Supports creating, reading, updating, and deleting invoices, bills, customers, and vendors.

## Installation

Add this line to your application's Gemfile:
Add to your Gemfile:

```ruby
gem 'intacct'
```

Then run:

```
bundle install
```

## Configuration

Call `Intacct.setup` once at startup (e.g. in a Rails initializer):

```ruby
Intacct.setup do |config|
config.xml_sender_id = ENV['INTACCT_XML_SENDER_ID'] # Web Services sender ID issued by Intacct
config.xml_password = ENV['INTACCT_XML_PASSWORD'] # Web Services sender password
config.app_user_id = ENV['INTACCT_USER_ID'] # Your Intacct user login
config.app_company_id = ENV['INTACCT_COMPANY_ID'] # Your Intacct company ID
config.app_password = ENV['INTACCT_PASSWORD'] # Your Intacct user password

# Optional prefixes prepended to IDs when creating records
config.invoice_prefix = 'INV-'
config.bill_prefix = 'BILL-'
config.customer_prefix = 'C'
config.vendor_prefix = 'V'

# Optional: override the Intacct gateway URL (useful for testing)
# config.service_url = 'https://www.intacct.com/ia/xml/xmlgw.phtml'

# Optional: HTTP timeouts in seconds
# config.http_open_timeout = 5
# config.http_read_timeout = 30
end
```

Use a `.env` file (via [dotenv](https://github.com/bkeepers/dotenv)) to keep credentials out of source control:

```
INTACCT_XML_SENDER_ID=your_sender_id
INTACCT_XML_PASSWORD=your_sender_password
INTACCT_USER_ID=your_user
INTACCT_COMPANY_ID=your_company
INTACCT_PASSWORD=your_password
```

## Testing Your Connection

After configuration, verify your credentials with a single call:

```ruby
Intacct.ping # => true if credentials are valid, false otherwise
```

`ping` sends a minimal authenticated request (fetching up to 1 customer) and returns `true` on success or `false` on any failure, including network errors and invalid credentials.

## Reading Data

```ruby
# List invoices (returns a QueryResult)
result = Intacct::Invoice.list
result.records # => Nokogiri::NodeSet of <invoice> elements

# List with a limit
result = Intacct::Invoice.list(limit: 50)

# List with a filter block
result = Intacct::Invoice.list do |xml|
xml.filter {
xml.expression {
xml.field "invoiceno"
xml.operator "="
xml.value "INV-12345"
}
}
end

# List bills
result = Intacct::Bill.list

# Find a customer by their Intacct ID
result = Intacct::Customer.find(id: "C12345", fields: [:customerid, :name, :termname])
result.data # => OpenStruct with the requested fields
```

## Writing Data

```ruby
# Create a customer
customer = OpenStruct.new(id: "12345", name: "Acme Corp")
Intacct::Customer.new(customer).create

# Create a vendor
vendor = OpenStruct.new(id: "V001", first_name: "John", last_name: "Doe", ...)
Intacct::Vendor.new(vendor).create

# Create an invoice (also creates customer/vendor if they don't have an intacct_system_id)
data = OpenStruct.new(invoice: invoice_obj, customer: customer_obj, vendor: vendor_obj)
intacct_invoice = Intacct::Invoice.new(data)
intacct_invoice.create # => true on success

# Create a bill
data = OpenStruct.new(payment: payment_obj, customer: customer_obj, vendor: vendor_obj)
intacct_bill = Intacct::Bill.new(data)
intacct_bill.create # => true on success
```

## Customizing XML (Important)

The gem handles authentication, request wrapping, and the structural skeleton of each resource. **It cannot know your account's custom fields or exact XML structure** — these are specific to your Intacct configuration.

You define field mappings locally using hooks inside the `Intacct.setup` block:

```ruby
Intacct.setup do |config|
# ... credentials ...

config.invoice do |inv|
inv.custom_fields do |xml|
# xml is a Nokogiri Builder — add whatever fields your account requires
xml.customfields {
xml.customfield {
xml.customfieldname "YOUR_CUSTOM_FIELD"
xml.customfieldvalue object.invoice.some_value
}
}
xml.invoiceitems {
xml.lineitem {
xml.glaccountno 4000
xml.amount object.invoice.total
xml.memo object.invoice.description
}
}
end
end

config.bill do |b|
b.custom_fields do |xml|
xml.billno object.payment.id
xml.description object.payment.note
end
end
end
```

Inside hook blocks, `object` refers to the data object passed to the resource constructor. See `spec/steps/intacct_invoice_steps.rb` for a detailed real-world example of invoice and bill field mappings.

### Available hooks

| Hook | Resource | Purpose |
|------|----------|---------|
| `invoice.custom_fields` | Invoice | XML appended inside `create_invoice` |
| `bill.custom_fields` | Bill | Extra fields inside `create_bill` |
| `bill.bill_item_fields` | Bill | Line items inside `create_bill` |
| `bill.before_create` | Bill | Runs before the HTTP request (e.g. set a pay date) |

You can also set hooks directly on the class for more control:

```ruby
Intacct::Invoice.custom_invoice_fields do |xml|
# ...
end

Intacct::Bill.bill_item_fields do |xml|
xml.billitems {
xml.lineitem {
xml.glaccountno 5000
xml.amount object.payment.amount
}
}
end
```

## Running Tests

### Unit tests (no credentials required)

gem 'intacct'
```
bundle exec rspec spec/intacct_spec.rb
```

And then execute:
HTTP is stubbed with WebMock — no Intacct account needed.

$ bundle
### Integration tests (real API calls)

Or install it yourself as:
1. Copy the example env file and fill in your credentials:

$ gem install intacct
```
cp .env.example .env
```

## Usage
2. Run a specific feature:

TODO: Write usage instructions here
```
INTACCT_INTEGRATION=1 bundle exec rspec spec/features/intacct_connection.feature
INTACCT_INTEGRATION=1 bundle exec rspec spec/features/intacct_invoice.feature
```

## Contributing
3. Run all integration tests:

1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
```
INTACCT_INTEGRATION=1 bundle exec rspec spec/features/
```
10 changes: 6 additions & 4 deletions intacct.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ require 'intacct/version'
Gem::Specification.new do |spec|
spec.name = "intacct"
spec.version = Intacct::VERSION
spec.authors = ["CJ Lazell"]
spec.email = ["cjlazell@gmail.com"]
spec.authors = ['CJ Lazell', 'Bernardo Espinoza']
spec.email = ['cjlazell@gmail.com', 'bernardo466@gmail.com']
spec.description = %q{Ruby lib to communicate with the Intacct API system.}
spec.summary = %q{Ruby Intacct API Client}
spec.homepage = ""
Expand All @@ -20,12 +20,14 @@ Gem::Specification.new do |spec|

spec.add_dependency "nokogiri"
spec.add_dependency "hooks"
spec.add_development_dependency "bundler", "~> 1.3"
spec.add_dependency "activesupport", ">= 6.0"
spec.add_development_dependency "bundler", ">= 1.3"
spec.add_development_dependency "rake"
spec.add_development_dependency "rspec"
spec.add_development_dependency "rspec", ">= 3.10"
spec.add_development_dependency "turnip"
spec.add_development_dependency "awesome_print"
spec.add_development_dependency "pry"
spec.add_development_dependency "dotenv-rails"
spec.add_development_dependency "faker", ">=1.2.0"
spec.add_development_dependency "webmock"
end
41 changes: 27 additions & 14 deletions lib/intacct.rb
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
require "intacct/version"
require 'ostruct'
require 'net/http'
require 'nokogiri'
require 'hooks'
require 'logger'
require 'active_support/core_ext/object/blank'
require "intacct/base"
require "intacct/error"
require "intacct/query_result"
require "intacct/customer"
require "intacct/vendor"
require "intacct/invoice"
require "intacct/bill"

class Object
def blank?
respond_to?(:empty?) ? empty? : !self
end

def present?
!blank?
end
end
require "intacct/resource_config"

module Intacct
extend self

attr_accessor :xml_sender_id , :xml_password ,
:app_user_id , :app_company_id , :app_password ,
:invoice_prefix , :bill_prefix ,
:vendor_prefix , :customer_prefix, :system_name
:app_user_id , :app_company_id ,
:app_password , :invoice_prefix ,
:bill_prefix , :vendor_prefix ,
:customer_prefix, :system_name ,
:service_url , :customer_fields ,
:http_open_timeout, :http_read_timeout

def setup
yield self
config = ResourceConfig.new
yield config
config.apply!
config
end

def logger
@logger ||= ::Logger.new($stdout).tap { |l| l.level = ::Logger::WARN }
end

def logger=(log)
@logger = log
end

def ping
Base.ping
end
end
Loading