diff --git a/.document b/.document deleted file mode 100644 index 3d618dd..0000000 --- a/.document +++ /dev/null @@ -1,5 +0,0 @@ -lib/**/*.rb -bin/* -- -features/**/*.feature -LICENSE.txt diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..85e723e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..574b02b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: Ruby + +on: + push: + branches: + - master + + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + strategy: + matrix: + ruby: + - "3.4.5" + + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run the default task + run: bundle exec rake diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8a6355e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release Gem + +on: + push: + tags: + - 'v*' + +jobs: + release: + name: Release gem to RubyGems.org + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4.5" + bundler-cache: true + + - uses: rubygems/release-gem@v1 diff --git a/.gitignore b/.gitignore index 25fc290..b20189a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,52 +1,31 @@ -# rcov generated -coverage -coverage.data - -# rdoc generated -rdoc - -# yard generated -doc -.yardoc - -# bundler -.bundle +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/tmp/ + +# Test coverage +/coverage/ + +# YARD documentation +/.yardoc/ +/doc/ +/_yardoc/ + +# RubyGems +*.gem +Gemfile.lock -# juwelier generated -pkg +# Bundler +/vendor/bundle/ -# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: -# -# * Create a file at ~/.gitignore -# * Include files you want ignored -# * Run: git config --global core.excludesfile ~/.gitignore -# -# After doing this, these files will be ignored in all your git projects, -# saving you from having to 'pollute' every project you touch with them -# -# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) -# -# For MacOS: -# +# Mac finder artifacts .DS_Store -# For TextMate -#*.tmproj -#tmtags - -# For emacs: -#*~ -#\#* -#.\#* - -# For vim: -#*.swp - -.idea/ - -# For redcar: -#.redcar - -# For rubinius: -#*.rbc -Gemfile.lock +AGENTS.md +CLAUDE.md +.claude/ +.worktrees/ +.repoprompt/ diff --git a/.rspec b/.rspec deleted file mode 100644 index 4e1e0d2..0000000 --- a/.rspec +++ /dev/null @@ -1 +0,0 @@ ---color diff --git a/.rubocop.yml b/.rubocop.yml index c0c3fac..4b480ab 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,15 +1,53 @@ AllCops: + TargetRubyVersion: 3.2 NewCops: enable SuggestExtensions: false Exclude: - - '*.gemspec' - - 'Rakefile' - - 'spec/spec_helper.rb' + - 'vendor/**/*' + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/Documentation: + Enabled: false Metrics/MethodLength: Max: 20 + Exclude: + - 'lib/intacct/functions/query.rb' -Style/Documentation: +Metrics/ClassLength: + Max: 150 + Exclude: + - 'test/**/*' + +Metrics/ModuleLength: + Max: 150 + Exclude: + - 'test/**/*' + +Metrics/AbcSize: + Exclude: + - 'lib/intacct/functions/query.rb' + +Metrics/CyclomaticComplexity: + Exclude: + - 'lib/intacct/functions/query.rb' + +Metrics/PerceivedComplexity: + Exclude: + - 'lib/intacct/functions/query.rb' + +Metrics/BlockLength: + Exclude: + - 'test/**/*' + - 'lib/intacct/functions/query.rb' + - '*.gemspec' + +Naming/AccessorMethodName: + Enabled: false + +Gemspec/DevelopmentDependencies: Enabled: false Style/StringConcatenation: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 48a2223..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ - -language: ruby -rvm: - - 3.1.1 - - 3.0.0 - - 2.2.3 - -script: bundle exec rspec spec diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..0bcdd63 --- /dev/null +++ b/.yardopts @@ -0,0 +1,6 @@ +--markup markdown +--no-private +lib/**/*.rb +- README.md +- CHANGELOG.md +- LICENSE.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..240fa37 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-11-13 + +### Changed +- **BREAKING**: Minimum Ruby version raised to 3.2.0 +- Migrated from RSpec to Minitest +- Modernized gemspec with comprehensive metadata +- Replaced Juwelier with standard Bundler gem tasks +- Updated RuboCop configuration for Ruby 3.2 +- Added frozen string literal pragma to all files + +### Added +- YARD documentation with comprehensive examples +- Complete test coverage with Minitest +- CHANGELOG.md (this file) +- Enhanced README with usage examples and badges +- RuboCop to default rake task + +### Removed +- Support for Ruby < 3.2 +- RSpec test framework +- Juwelier gem management +- Legacy coverage tools (CodeClimate, Coveralls) + +## [0.0.15] - Previous Release + +Historical changes before 1.0.0 modernization. diff --git a/Gemfile b/Gemfile index 4c160e0..0ca3e7d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,20 +1,7 @@ -source 'https://rubygems.org' +# frozen_string_literal: true -gem 'faraday', '~> 1.10.0' -gem 'json', '~> 2.7' +source "https://rubygems.org" -group :development do - gem 'bundler' - gem 'juwelier', '~> 2.4.9' - gem 'pry-byebug' - gem 'rdoc', '~> 6.0' - gem 'rspec', '~> 3.11.0' - gem 'shoulda', '~> 4.0.0' - gem 'simplecov', '>= 0.21.0' - gem 'timecop', '~> 0.9.0' - gem 'webmock', '~> 3.18.0' +gemspec - gem 'codeclimate-test-reporter', require: false - gem 'coveralls', require: false - gem 'rubocop', require: false -end +gem "rake", "~> 13.0" diff --git a/README.md b/README.md index be1ece1..6eb4394 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,281 @@ -# Intacct Sage Ruby wrapper +# Intacct Ruby Gem -This is Ruby wrapper to [Intacct Sage API](https://developer.intacct.com/api/) +[![Gem Version](https://badge.fury.io/rb/intacct.svg)](https://badge.fury.io/rb/intacct) +[![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/intacct) +[![CI](https://github.com/dpaluy/intacct/workflows/CI/badge.svg)](https://github.com/dpaluy/intacct/actions) -## Status - -TBD +A Ruby gem for interacting with the [Sage Intacct API](https://developer.intacct.com/api/). ## Installation Add this line to your application's Gemfile: -`gem 'intacct'` +```ruby +gem "intacct" +``` + +**Requirements:** Ruby 3.2 or higher. ## Usage -``` +### Configuration + +Configure the gem with your Intacct credentials: + +```ruby Intacct.configure do |config| - config.sender_id = 'SENDER-ID-FROM-SAGE' - config.sender_password = 'SENDER-PASSWORD-FROM-SAGE' + config.sender_id = ENV["INTACCT_SENDER_ID"] + config.sender_password = ENV["INTACCT_SENDER_PASSWORD"] +end +``` + +**Security Note:** Never commit credentials to version control. Use environment variables or a secrets management system. + +### Making Requests + +#### Query Records + +Query Intacct objects with filters and sorting: + +```ruby +query = Intacct::Functions::Query.new( + object: "CUSTOMER", + select: ["CUSTOMERID", "NAME", "EMAIL", "STATUS"], + filter: { "STATUS" => "active" }, + order: { "NAME" => "asc" } +) + +request = Intacct::Request.new +request.use_credentials_authentication( + user_id: ENV["INTACCT_USER_ID"], + company_id: ENV["INTACCT_COMPANY_ID"], + user_password: ENV["INTACCT_USER_PASSWORD"] +) +request.add_function(query, "query-001") + +response = Intacct::Gateway.new.send_request(request) + +if response.successful? + result = response.get_function_result("query-001") + if result.successful? + customers = result.data + customers.each do |customer| + puts "#{customer['CUSTOMERID']}: #{customer['NAME']}" + end + else + puts "Function error: #{result.errors.join(', ')}" + end +else + puts "Request failed" end ``` -TBD +#### Create Records + +Create new records in Intacct: + +```ruby +create = Intacct::Functions::Create.new( + object: "CUSTOMER", + fields: { + "CUSTOMERID" => "C-001", + "NAME" => "Acme Corporation", + "EMAIL" => "contact@acme.com", + "STATUS" => "active" + } +) + +request = Intacct::Request.new +request.use_credentials_authentication( + user_id: ENV["INTACCT_USER_ID"], + company_id: ENV["INTACCT_COMPANY_ID"], + user_password: ENV["INTACCT_USER_PASSWORD"] +) +request.add_function(create, "create-001") + +response = Intacct::Gateway.new.send_request(request) + +if response.successful? + result = response.get_function_result("create-001") + puts "Customer created!" if result.successful? +end +``` + +#### Update Records + +Update existing records: + +```ruby +update = Intacct::Functions::Update.new( + object: "CUSTOMER", + keys: { "CUSTOMERID" => "C-001" }, + fields: { + "EMAIL" => "newemail@acme.com", + "STATUS" => "inactive" + } +) + +request = Intacct::Request.new +request.use_credentials_authentication( + user_id: ENV["INTACCT_USER_ID"], + company_id: ENV["INTACCT_COMPANY_ID"], + user_password: ENV["INTACCT_USER_PASSWORD"] +) +request.add_function(update, "update-001") + +response = Intacct::Gateway.new.send_request(request) +``` + +#### Read by ID + +Read a specific record by ID: + +```ruby +read = Intacct::Functions::Read.new( + object: "CUSTOMER", + keys: "C-001", + fields: ["CUSTOMERID", "NAME", "EMAIL"] +) + +request = Intacct::Request.new +request.use_credentials_authentication( + user_id: ENV["INTACCT_USER_ID"], + company_id: ENV["INTACCT_COMPANY_ID"], + user_password: ENV["INTACCT_USER_PASSWORD"] +) +request.add_function(read, "read-001") + +response = Intacct::Gateway.new.send_request(request) +``` + +#### Case-Insensitive Queries + +Perform case-insensitive queries: + +```ruby +query = Intacct::Functions::Query.new( + object: "CUSTOMER", + select: ["CUSTOMERID", "NAME"], + filter: { "NAME" => "acme" }, + case_insensitive: true +) +``` + +### Error Handling + +Always check response status and handle errors: + +```ruby +response = Intacct::Gateway.new.send_request(request) + +if response.successful? + result = response.get_function_result("my-control-id") + if result.successful? + puts "Success: #{result.data}" + else + puts "Function error: #{result.errors.join(', ')}" + end +else + puts "Request failed" +end +``` + +### Session Authentication + +Get an API session for subsequent requests: + +```ruby +get_session = Intacct::Functions::GetApiSession.new(location_id: "US") + +request = Intacct::Request.new +request.use_credentials_authentication( + user_id: ENV["INTACCT_USER_ID"], + company_id: ENV["INTACCT_COMPANY_ID"], + user_password: ENV["INTACCT_USER_PASSWORD"] +) +request.add_function(get_session, "session-001") + +response = Intacct::Gateway.new.send_request(request) + +if response.successful? + result = response.get_function_result("session-001") + if result.successful? + session_id = result.data["sessionid"] + # Use session_id for subsequent requests + end +end +``` + +## Available Functions + +- **Query** - Execute read queries with filters and sorting +- **Create** - Create new records +- **Read** - Read specific records by ID +- **Update** - Update existing records +- **CreateArAdjustment** - Create AR adjustments +- **GetApiSession** - Get API session for authentication +- **RetrievePdf** - Retrieve PDF documents +- **ReversePayment** - Reverse payments + +For detailed API documentation, see [RubyDoc](https://rubydoc.info/gems/intacct). + +## Development + +After checking out the repo, run: + +```bash +bundle install +``` + +Run tests: + +```bash +bundle exec rake test +``` + +Run linter: + +```bash +bundle exec rubocop +``` + +Generate documentation: + +```bash +bundle exec yard doc +bundle exec yard server # View docs at http://localhost:8808 +``` + +To install this gem onto your local machine: + +```bash +bundle exec rake install +``` + +To release a new version: + +1. Update version in `lib/intacct/version.rb` +2. Update `CHANGELOG.md` +3. Commit changes +4. Run `bundle exec rake release` (creates tag and pushes to RubyGems) + +## Contributing + +1. Fork it (https://github.com/dpaluy/intacct/fork) +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 a new Pull Request + +Please ensure: +- Tests pass (`bundle exec rake test`) +- Code follows style guide (`bundle exec rubocop`) +- New features include tests and documentation -## Contributing to intacct gem +## License -* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. -* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. -* Fork the project. -* Start a feature/bugfix branch. -* Commit and push until you are happy with your contribution. -* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. -* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. +The gem is available as open source under the terms of the [MIT License](LICENSE.txt). ## Copyright -Copyright (c) 2022 David Paluy. See LICENSE.txt for -further details. +Copyright (c) 2022-2025 David Paluy. See LICENSE.txt for further details. diff --git a/Rakefile b/Rakefile index 1a1e108..c1fc819 100644 --- a/Rakefile +++ b/Rakefile @@ -1,57 +1,21 @@ -# encoding: utf-8 +# frozen_string_literal: true -require 'rubygems' -require 'bundler' -begin - Bundler.setup(:default, :development) -rescue Bundler::BundlerError => e - $stderr.puts e.message - $stderr.puts "Run `bundle install` to install missing gems" - exit e.status_code -end -require 'rake' -require 'juwelier' -Juwelier::Tasks.new do |gem| - # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options - gem.name = "intacct" - gem.homepage = "http://github.com/dpaluy/intacct" - gem.license = "MIT" - gem.summary = %Q{Sage Intacct API wrapper} - gem.description = %Q{Sage Intacct API wrapper} - gem.email = "dpaluy@users.noreply.github.com" - gem.authors = ["David Paluy", "Yaroslav Konovets"] +require "bundler/gem_tasks" +require "minitest/test_task" - # dependencies defined in Gemfile -end -Juwelier::RubygemsDotOrgTasks.new - -require 'rake/testtask' -Rake::TestTask.new(:test) do |test| - test.libs << 'lib' << 'test' - test.pattern = 'test/**/test_*.rb' - test.verbose = true -end +Minitest::TestTask.create -require 'rspec/core' -require 'rspec/core/rake_task' -RSpec::Core::RakeTask.new(:spec) do |spec| - spec.pattern = FileList['spec/**/*_spec.rb'] -end +require "rubocop/rake_task" +RuboCop::RakeTask.new -desc 'Code coverage detail' -task :simplecov do - ENV['COVERAGE'] = 'true' - Rake::Task['spec'].execute +begin + require "yard" + YARD::Rake::YardocTask.new do |t| + t.files = ["lib/**/*.rb"] + t.options = ["--markup", "markdown", "--no-private"] + end +rescue LoadError + # YARD not available end -task default: :spec - -require 'rdoc/task' -Rake::RDocTask.new do |rdoc| - version = File.exist?('VERSION') ? File.read('VERSION') : '' - - rdoc.rdoc_dir = 'rdoc' - rdoc.title = "intacct #{version}" - rdoc.rdoc_files.include('README*') - rdoc.rdoc_files.include('lib/**/*.rb') -end +task default: %i[test rubocop] diff --git a/intacct.gemspec b/intacct.gemspec index 1d086d8..3da49cf 100644 --- a/intacct.gemspec +++ b/intacct.gemspec @@ -1,79 +1,42 @@ -# Generated by juwelier -# DO NOT EDIT THIS FILE DIRECTLY -# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec' -# -*- encoding: utf-8 -*- -# stub: intacct 0 ruby lib +# frozen_string_literal: true -Gem::Specification.new do |s| - s.name = "intacct".freeze - s.version = "0".freeze +require_relative "lib/intacct/version" - s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= - s.require_paths = ["lib".freeze] - s.authors = ["David Paluy".freeze, "Yaroslav Konovets".freeze] - s.date = "1980-01-02" - s.description = "Sage Intacct API wrapper".freeze - s.email = "dpaluy@users.noreply.github.com".freeze - s.extra_rdoc_files = [ - "LICENSE.txt", - "README.md" - ] - s.files = [ - ".document", - ".rspec", - ".rubocop.yml", - ".travis.yml", - "Gemfile", - "LICENSE.txt", - "README.md", - "Rakefile", - "intacct.gemspec", - "lib/intacct.rb", - "lib/intacct/authentication_methods/credentials.rb", - "lib/intacct/authentication_methods/session.rb", - "lib/intacct/authentication_result.rb", - "lib/intacct/config.rb", - "lib/intacct/exceptions/client_exception.rb", - "lib/intacct/exceptions/function_failure_exception.rb", - "lib/intacct/exceptions/missing_authentication_exception.rb", - "lib/intacct/function_result.rb", - "lib/intacct/functions/create.rb", - "lib/intacct/functions/create_ar_adjustment.rb", - "lib/intacct/functions/get_api_session.rb", - "lib/intacct/functions/query.rb", - "lib/intacct/functions/read.rb", - "lib/intacct/functions/retrieve_pdf.rb", - "lib/intacct/functions/reverse_payment.rb", - "lib/intacct/functions/update.rb", - "lib/intacct/gateway.rb", - "lib/intacct/request.rb", - "lib/intacct/response.rb", - "lib/intacct/utils.rb", - "lib/intacct/version.rb", - "spec/intacct/client_spec.rb", - "spec/intacct_spec.rb", - "spec/spec_helper.rb" - ] - s.homepage = "http://github.com/dpaluy/intacct".freeze - s.licenses = ["MIT".freeze] - s.rubygems_version = "3.7.2".freeze - s.summary = "Sage Intacct API wrapper".freeze +Gem::Specification.new do |spec| + spec.name = "intacct" + spec.version = Intacct::VERSION + spec.authors = ["David Paluy", "Yaroslav Konovets"] + spec.required_ruby_version = ">= 3.2.0" + spec.summary = "A Ruby wrapper for the Intacct API" + spec.license = "MIT" - s.specification_version = 4 + spec.homepage = "https://github.com/dpaluy/intacct" + spec.metadata["rubygems_mfa_required"] = "true" + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/intacct" + spec.metadata["source_code_uri"] = "https://github.com/dpaluy/intacct" + spec.metadata["changelog_uri"] = "https://github.com/dpaluy/intacct/blob/master/CHANGELOG.md" + spec.metadata["bug_tracker_uri"] = "https://github.com/dpaluy/intacct/issues" - s.add_runtime_dependency(%q.freeze, ["~> 1.10.0".freeze]) - s.add_runtime_dependency(%q.freeze, ["~> 2.7".freeze]) - s.add_development_dependency(%q.freeze, [">= 0".freeze]) - s.add_development_dependency(%q.freeze, ["~> 2.4.9".freeze]) - s.add_development_dependency(%q.freeze, [">= 0".freeze]) - s.add_development_dependency(%q.freeze, ["~> 6.0".freeze]) - s.add_development_dependency(%q.freeze, ["~> 3.11.0".freeze]) - s.add_development_dependency(%q.freeze, ["~> 4.0.0".freeze]) - s.add_development_dependency(%q.freeze, [">= 0.21.0".freeze]) - s.add_development_dependency(%q.freeze, ["~> 0.9.0".freeze]) - s.add_development_dependency(%q.freeze, ["~> 3.18.0".freeze]) - s.add_development_dependency(%q.freeze, [">= 0".freeze]) - s.add_development_dependency(%q.freeze, [">= 0".freeze]) - s.add_development_dependency(%q.freeze, [">= 0".freeze]) -end + gemspec = File.basename(__FILE__) + spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| + ls.readlines("\x0", chomp: true).reject do |f| + (f == gemspec) || + f.start_with?(*%w[bin/ test/ spec/ .github/ .gitignore .rspec .rubocop.yml Gemfile]) + end + end + + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE.txt"] + spec.add_dependency "builder", "~> 3.0" + + spec.add_development_dependency "minitest", "~> 5.0" + spec.add_development_dependency "rake", "~> 13.0" + spec.add_development_dependency "rubocop", "~> 1.21" + spec.add_development_dependency "webmock", "~> 3.23" + spec.add_development_dependency "yard", "~> 0.9" +end diff --git a/lib/intacct.rb b/lib/intacct.rb index dc6111c..b285252 100644 --- a/lib/intacct.rb +++ b/lib/intacct.rb @@ -1,18 +1,57 @@ -require_relative 'intacct/utils' -require_relative 'intacct/config' -require_relative 'intacct/gateway' -require_relative 'intacct/request' -require_relative 'intacct/functions/create' -require_relative 'intacct/functions/create_ar_adjustment' -require_relative 'intacct/functions/update' -require_relative 'intacct/functions/get_api_session' -require_relative 'intacct/functions/query' -require_relative 'intacct/functions/read' -require_relative 'intacct/functions/retrieve_pdf' -require_relative 'intacct/functions/reverse_payment' +# frozen_string_literal: true + +require_relative "intacct/version" +require_relative "intacct/utils" +require_relative "intacct/config" +require_relative "intacct/gateway" +require_relative "intacct/request" +require_relative "intacct/functions/create" +require_relative "intacct/functions/create_ar_adjustment" +require_relative "intacct/functions/update" +require_relative "intacct/functions/get_api_session" +require_relative "intacct/functions/query" +require_relative "intacct/functions/read" +require_relative "intacct/functions/retrieve_pdf" +require_relative "intacct/functions/reverse_payment" module Intacct - def self.logger - config.logger + class << self + # Returns the global configuration instance. + # + # @return [Config] the current configuration object + def config + @config ||= Config.new + end + + # Configures the Intacct client globally. + # + # @example Configure with credentials + # Intacct.configure do |config| + # config.sender_id = ENV["INTACCT_SENDER_ID"] + # config.sender_password = ENV["INTACCT_SENDER_PASSWORD"] + # config.user_id = ENV["INTACCT_USER_ID"] + # config.user_password = ENV["INTACCT_USER_PASSWORD"] + # config.company_id = ENV["INTACCT_COMPANY_ID"] + # end + # + # @yield [config] Gives the configuration object to the block + # @yieldparam config [Config] the configuration instance to modify + # @return [void] + def configure + yield(config) + end + + # Resets the global configuration to nil. + # + # Primarily used for testing to ensure a clean configuration state. + # + # @return [void] + def reset_configuration! + @config = nil + end + + def logger + config.logger + end end end diff --git a/lib/intacct/authentication_methods/credentials.rb b/lib/intacct/authentication_methods/credentials.rb index 1dcbe52..e9627f5 100644 --- a/lib/intacct/authentication_methods/credentials.rb +++ b/lib/intacct/authentication_methods/credentials.rb @@ -1,4 +1,6 @@ -require 'builder' +# frozen_string_literal: true + +require "builder" module Intacct module AuthenticationMethods diff --git a/lib/intacct/authentication_methods/session.rb b/lib/intacct/authentication_methods/session.rb index 41ac42d..801eea8 100644 --- a/lib/intacct/authentication_methods/session.rb +++ b/lib/intacct/authentication_methods/session.rb @@ -1,4 +1,6 @@ -require 'builder' +# frozen_string_literal: true + +require "builder" module Intacct module AuthenticationMethods diff --git a/lib/intacct/authentication_result.rb b/lib/intacct/authentication_result.rb index 2d5bc08..7ab6041 100644 --- a/lib/intacct/authentication_result.rb +++ b/lib/intacct/authentication_result.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + module Intacct class AuthenticationResult attr_reader :status, :xml_data def initialize(xml_entry) - @status = xml_entry.xpath('status').text + @status = xml_entry.xpath("status").text @xml_data = xml_entry end def successful? - @status == 'success' + @status == "success" end def parsed_data diff --git a/lib/intacct/config.rb b/lib/intacct/config.rb index db9fd6a..e76dad2 100644 --- a/lib/intacct/config.rb +++ b/lib/intacct/config.rb @@ -1,20 +1,30 @@ -require 'logger' +# frozen_string_literal: true + +require "logger" module Intacct + # Configuration object for Intacct API credentials and settings. + # + # @attr sender_id [String] Intacct Web Services sender ID + # @attr sender_password [String] Intacct Web Services sender password + # @attr url [String] Custom API endpoint URL (optional) + # @attr raise_exceptions [Boolean] Whether to raise exceptions on API errors (default: true) + # @attr logger [Logger] Logger instance for debugging (default: ERROR level stdout logger) + # + # @example Basic configuration + # config = Intacct::Config.new + # config.sender_id = "my-sender-id" + # config.sender_password = "my-sender-password" + # config.raise_exceptions = false class Config attr_accessor :sender_id, :sender_password, :url, :raise_exceptions, :logger + # Initialize a new configuration with default logger and exception handling + # + # @return [Config] a new configuration instance def initialize @logger = Logger.new($stdout, level: Logger::Severity::ERROR) @raise_exceptions = true end end - - def self.configure - yield config - end - - def self.config - @config ||= Config.new - end end diff --git a/lib/intacct/exceptions/client_exception.rb b/lib/intacct/exceptions/client_exception.rb index df133cb..90dccd2 100644 --- a/lib/intacct/exceptions/client_exception.rb +++ b/lib/intacct/exceptions/client_exception.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Exceptions class ClientException < StandardError; end diff --git a/lib/intacct/exceptions/function_failure_exception.rb b/lib/intacct/exceptions/function_failure_exception.rb index c2fb841..d837ed3 100644 --- a/lib/intacct/exceptions/function_failure_exception.rb +++ b/lib/intacct/exceptions/function_failure_exception.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Exceptions class FunctionFailureException < StandardError; end diff --git a/lib/intacct/exceptions/missing_authentication_exception.rb b/lib/intacct/exceptions/missing_authentication_exception.rb index e100ec3..89ce4bb 100644 --- a/lib/intacct/exceptions/missing_authentication_exception.rb +++ b/lib/intacct/exceptions/missing_authentication_exception.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Exceptions class MissingAuthenticationException < StandardError; end diff --git a/lib/intacct/function_result.rb b/lib/intacct/function_result.rb index 6c82cb6..b142dff 100644 --- a/lib/intacct/function_result.rb +++ b/lib/intacct/function_result.rb @@ -1,20 +1,22 @@ +# frozen_string_literal: true + module Intacct class FunctionResult attr_reader :status, :control_id, :xml_data def initialize(xml_entry) - @status = xml_entry.xpath('status').text - @control_id = xml_entry.xpath('controlid').text + @status = xml_entry.xpath("status").text + @control_id = xml_entry.xpath("controlid").text @xml_data = xml_entry end def successful? - @status == 'success' + @status == "success" end def parsed_data hash = Crack::XML.parse(@xml_data.to_s).with_indifferent_access - successful? ? hash.fetch('result') : hash + successful? ? hash.fetch("result") : hash end def push_error_messages @@ -23,7 +25,7 @@ def push_error_messages error_details = [error_details] unless error_details.is_a?(Array) error_details.compact.map do |error_data| - [error_data[:description2], error_data[:correction]].compact.join(' ') + [error_data[:description2], error_data[:correction]].compact.join(" ") end end end diff --git a/lib/intacct/functions/create.rb b/lib/intacct/functions/create.rb index 9d1ce17..52d15cc 100644 --- a/lib/intacct/functions/create.rb +++ b/lib/intacct/functions/create.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Functions class Create diff --git a/lib/intacct/functions/create_ar_adjustment.rb b/lib/intacct/functions/create_ar_adjustment.rb index 5a2bc27..154d251 100644 --- a/lib/intacct/functions/create_ar_adjustment.rb +++ b/lib/intacct/functions/create_ar_adjustment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Functions class CreateArAdjustment diff --git a/lib/intacct/functions/get_api_session.rb b/lib/intacct/functions/get_api_session.rb index 7f56ca1..7d51536 100644 --- a/lib/intacct/functions/get_api_session.rb +++ b/lib/intacct/functions/get_api_session.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Functions class GetApiSession diff --git a/lib/intacct/functions/query.rb b/lib/intacct/functions/query.rb index 4d72dc7..72353fc 100644 --- a/lib/intacct/functions/query.rb +++ b/lib/intacct/functions/query.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Functions class Query @@ -57,9 +59,9 @@ def to_xml def to_direction(direction) case direction when :asc - 'ascending' + "ascending" when :desc - 'descending' + "descending" else raise ArgumentError, "Invalid direction: #{direction}" end diff --git a/lib/intacct/functions/read.rb b/lib/intacct/functions/read.rb index 818a1f8..c2798bc 100644 --- a/lib/intacct/functions/read.rb +++ b/lib/intacct/functions/read.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Functions class Read @@ -14,8 +16,8 @@ def to_xml builder = Builder::XmlMarkup.new builder.read do builder.object @object - builder.keys keys.join(',') - fields_value = @fields.any? ? @fields.join(',') : '*' + builder.keys keys.join(",") + fields_value = @fields.any? ? @fields.join(",") : "*" builder.fields fields_value args.each do |key, value| builder.tag!(key, value) diff --git a/lib/intacct/functions/retrieve_pdf.rb b/lib/intacct/functions/retrieve_pdf.rb index f8c17d0..82e0746 100644 --- a/lib/intacct/functions/retrieve_pdf.rb +++ b/lib/intacct/functions/retrieve_pdf.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Functions class RetrievePdf diff --git a/lib/intacct/functions/reverse_payment.rb b/lib/intacct/functions/reverse_payment.rb index 8047be1..38e39de 100644 --- a/lib/intacct/functions/reverse_payment.rb +++ b/lib/intacct/functions/reverse_payment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Functions class ReversePayment diff --git a/lib/intacct/functions/update.rb b/lib/intacct/functions/update.rb index 1d669c7..4e99d07 100644 --- a/lib/intacct/functions/update.rb +++ b/lib/intacct/functions/update.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Intacct module Functions class Update @@ -18,4 +20,3 @@ def to_xml end end end - diff --git a/lib/intacct/gateway.rb b/lib/intacct/gateway.rb index a1fb92e..fb4059e 100644 --- a/lib/intacct/gateway.rb +++ b/lib/intacct/gateway.rb @@ -1,8 +1,10 @@ -require 'intacct/response' +# frozen_string_literal: true + +require "intacct/response" module Intacct class Gateway - URI_STRING = 'https://api.intacct.com/ia/xml/xmlgw.phtml'.freeze + URI_STRING = "https://api.intacct.com/ia/xml/xmlgw.phtml" def initialize(control_config) @control_config = control_config @@ -12,7 +14,7 @@ def initialize(control_config) def execute_request(api_request) post_request ||= Net::HTTP::Post.new(uri.request_uri) - post_request['Content-Type'] = 'x-intacct-xml-request' + post_request["Content-Type"] = "x-intacct-xml-request" post_request.body = api_request.to_xml(@control_config) Intacct.logger.debug("Request Body: #{post_request.body}") http_response = @http_gateway.request(post_request) diff --git a/lib/intacct/request.rb b/lib/intacct/request.rb index e1b9c71..cbf672a 100644 --- a/lib/intacct/request.rb +++ b/lib/intacct/request.rb @@ -1,6 +1,8 @@ -require 'builder' -require 'intacct/authentication_methods/credentials' -require 'intacct/authentication_methods/session' +# frozen_string_literal: true + +require "builder" +require "intacct/authentication_methods/credentials" +require "intacct/authentication_methods/session" module Intacct class Request diff --git a/lib/intacct/response.rb b/lib/intacct/response.rb index 9f42219..c135c12 100644 --- a/lib/intacct/response.rb +++ b/lib/intacct/response.rb @@ -1,10 +1,28 @@ -require 'intacct/function_result' -require 'intacct/authentication_result' +# frozen_string_literal: true + +require "intacct/function_result" +require "intacct/authentication_result" module Intacct + # Parses and provides access to Intacct API XML responses. + # + # Wraps the HTTP response from Intacct and provides convenient methods + # to check status and extract function results. + # + # @example Parse a response + # response = Intacct::Response.new(http_response) + # if response.successful? + # result = response.get_function_result("my-control-id") + # puts result.data + # end class Response + # @return [Nokogiri::XML::Document] the parsed XML response body attr_reader :response_body + # Initialize a new Response from an HTTP response + # + # @param http_response [Net::HTTPResponse] the HTTP response from Intacct API + # @raise [Net::HTTPError] if the response status is not 2xx def initialize(http_response) @response_body = Nokogiri::XML(http_response.body) @@ -12,27 +30,37 @@ def initialize(http_response) http_response.value end + # Check if the overall response was successful + # + # @return [Boolean] true if the response control status is "success" def successful? - @response_body.xpath('//response/control/status').text == 'success' + @response_body.xpath("//response/control/status").text == "success" end + # Get the function result for a specific control ID + # + # @param control_id [String, Symbol] the control ID of the function + # @return [FunctionResult, nil] the function result or nil if not found def get_function_result(control_id) @function_results ||= build_function_results @function_results[control_id.to_s] end + # Get the authentication result from the response + # + # @return [AuthenticationResult] the authentication result def get_authentication_result - Intacct::AuthenticationResult.new(@response_body.xpath('//response/operation/authentication')) + Intacct::AuthenticationResult.new(@response_body.xpath("//response/operation/authentication")) end private def build_function_results - @response_body.xpath("//result").map do |xml_entry| + @response_body.xpath("//result").to_h do |xml_entry| function_result = Intacct::FunctionResult.new(xml_entry) [function_result.control_id, function_result] - end.to_h + end end end end diff --git a/lib/intacct/utils.rb b/lib/intacct/utils.rb index 3683ce0..61bf1f0 100644 --- a/lib/intacct/utils.rb +++ b/lib/intacct/utils.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module Intacct module Utils - YEAR_FORMAT = "%Y".freeze - MONTH_FORMAT = "%m".freeze - DAY_FORMAT = "%d".freeze + YEAR_FORMAT = "%Y" + MONTH_FORMAT = "%m" + DAY_FORMAT = "%d" DATE_FORMAT = "#{MONTH_FORMAT}/#{DAY_FORMAT}/#{YEAR_FORMAT}".freeze DATETIME_FORMAT = "#{MONTH_FORMAT}/#{DAY_FORMAT}/#{YEAR_FORMAT} %H:%M:%S".freeze diff --git a/lib/intacct/version.rb b/lib/intacct/version.rb index 1dd088b..f687417 100644 --- a/lib/intacct/version.rb +++ b/lib/intacct/version.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + module Intacct - VERSION = "0.0.15" + # Current version of the Intacct gem + VERSION = "1.0.0" end diff --git a/spec/intacct/client_spec.rb b/spec/intacct/client_spec.rb deleted file mode 100644 index 9876337..0000000 --- a/spec/intacct/client_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') - -xdescribe 'Intacct::Client' do - # TODO -end diff --git a/spec/intacct_spec.rb b/spec/intacct_spec.rb deleted file mode 100644 index 930f1bd..0000000 --- a/spec/intacct_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') - -describe 'Intacct' do - it 'has a version' do - expect(Intacct::VERSION).not_to be_nil - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 3407b1d..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'codeclimate-test-reporter' -require 'simplecov' -require 'coveralls' - -require 'webmock/rspec' - -if ENV['COVERAGE'] - WebMock.disable_net_connect!(allow: 'codeclimate.com') - - Coveralls.wear! - SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ - SimpleCov::Formatter::HTMLFormatter, - Coveralls::SimpleCov::Formatter - ] - - # SimpleCov.start - CodeClimate::TestReporter.start -end - -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -$LOAD_PATH.unshift(File.dirname(__FILE__)) - -require 'intacct' -require 'byebug' -require 'timecop' -require 'rspec' - -# Requires supporting files with custom matchers and macros, etc, -# in ./support/ and its subdirectories. -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } - -RSpec.configure do |config| - config.order = 'random' -end diff --git a/test/config_test.rb b/test/config_test.rb new file mode 100644 index 0000000..4ff8eb7 --- /dev/null +++ b/test/config_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "test_helper" + +class ConfigTest < Minitest::Test + include Intacct::TestHelper + + def test_config_attributes + config = Intacct::Config.new + + config.sender_id = "sender" + config.sender_password = "pass" + config.url = "https://custom.url" + + assert_equal "sender", config.sender_id + assert_equal "pass", config.sender_password + assert_equal "https://custom.url", config.url + end + + def test_default_logger + config = Intacct::Config.new + assert_instance_of Logger, config.logger + end + + def test_default_raise_exceptions + config = Intacct::Config.new + assert_equal true, config.raise_exceptions + end + + def test_custom_logger + custom_logger = Logger.new($stdout) + config = Intacct::Config.new + config.logger = custom_logger + assert_equal custom_logger, config.logger + end +end diff --git a/test/intacct_test.rb b/test/intacct_test.rb new file mode 100644 index 0000000..7efee48 --- /dev/null +++ b/test/intacct_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "test_helper" + +class IntacctTest < Minitest::Test + def test_has_version_number + refute_nil Intacct::VERSION + assert_match(/\d+\.\d+\.\d+/, Intacct::VERSION) + end + + def test_configure_yields_config + Intacct.configure do |config| + assert_instance_of Intacct::Config, config + end + end + + def test_reset_configuration + Intacct.configure do |config| + config.sender_id = "test-id" + end + + assert_equal "test-id", Intacct.config.sender_id + + Intacct.reset_configuration! + + assert_nil Intacct.config.sender_id + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..2e40cb7 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "intacct" + +require "minitest/autorun" +require "webmock/minitest" + +# Disable external HTTP requests +WebMock.disable_net_connect!(allow_localhost: true) + +module Intacct + module TestHelper + def setup + WebMock.reset! + Intacct.reset_configuration! + end + + def teardown + WebMock.reset! + Intacct.reset_configuration! + end + + def configure_intacct + Intacct.configure do |config| + config.sender_id = "test-sender-id" + config.sender_password = "test-sender-password" + config.user_id = "test-user-id" + config.user_password = "test-user-password" + config.company_id = "test-company" + end + end + + def stub_successful_intacct_request(response_body: "") + stub_request(:post, "https://api.intacct.com/ia/xml/xmlgw.phtml") + .to_return(status: 200, body: response_body, headers: { "Content-Type" => "text/xml" }) + end + end +end